Почему изменение состояния дочернего нижнего листа вызывает перестройку родительского виджета?

У меня есть экран Scaffold (ListsScreen). У которого есть кнопка (AddNewListButton), которая открывает модальный нижний лист (ListScreenBottomSheetWidget). Нижний лист имеет TextField (ListTitleInputTextFieldWidget).

Когда я нажимаю TextField, чтобы открыть клавиатуру, родительский экран перестраивается, из-за чего, конечно же, перестраиваются и все его дочерние виджеты.

Почему это происходит? У меня сложилось впечатление, что изменения состояния перестраивают только себя или своих детей, а не их родителей. И я также добавил конструкторы const почти везде, чтобы избежать перестроений, но это все еще происходит.

Экран списков родителей:

class ListsScreen extends StatelessWidget {
  const ListsScreen();
  static const routeName = '/lists-screen';

  @override
  Widget build(BuildContext context) {
    final user = Provider.of<AuthProvider>(context, listen: false).getUser;
    print('stateless rebuilding');
    return Scaffold(
      appBar: AppBar(
        centerTitle: false,
        title: Text(
          '${user['name']}\'s Lists',
          style: TextStyle(
            color: Theme.of(context).primaryColorLight,
          ),
        ),
        actions: <Widget>[
          const SignOutButton(),
        ],
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            children: <Widget>[
              SizeConfig.smallDevice
                  ? const SizedBox(
                      height: 30,
                    )
                  : const SizedBox(
                      height: 40,
                    ),
              SizeConfig.smallDevice
                  ? Text(
                      'Welcome to TODOS',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontSize: 20,
                        color: Colors.grey[700],
                      ),
                    )
                  : Text(
                      'Welcome to TODOS',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        fontSize: 25,
                        color: Colors.grey[700],
                      ),
                    ),
              SizeConfig.smallDevice
                  ? const SizedBox(
                      height: 30,
                    )
                  : const SizedBox(
                      height: 40,
                    ),
              const AddNewListButton(),
              SizeConfig.smallDevice
                  ? const SizedBox(
                      height: 30,
                    )
                  : const SizedBox(
                      height: 40,
                    ),
              const UserListsListViewWidget(),
            ],
          ),
        ),
      ),
    );
  }
}

class SignOutButton extends StatelessWidget {
  const SignOutButton();

  Future<void> _submitRequest(BuildContext context) async {
    _showLoadingAlert(context);

    try {
      await Provider.of<AuthProvider>(context, listen: false)
          .submitLogOutRequest();
      Navigator.of(context).pop();
      Navigator.of(context).pushReplacementNamed(LoginScreen.routeName);
    } on HttpExceptions catch (error) {
      Navigator.of(context).pop();
      _showErrorDialogue(error.getErrorList, context);
    }
  }

  void _showErrorDialogue(List<dynamic> errorMessages, BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) => Dialog(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20.0),
        ),
        child: ErrorListWidget(errorMessages: errorMessages),
      ),
    );
  }

  void _showLoadingAlert(BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) => const LoadingWidget(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      onPressed: () => _submitRequest(context),
      child: Row(
        children: <Widget>[
          Text(
            'Sign Out',
            style: TextStyle(
              color: Theme.of(context).primaryColorLight,
            ),
          ),
          Icon(
            Icons.exit_to_app,
            color: Theme.of(context).primaryColorLight,
          ),
        ],
      ),
    );
  }
}

class AddNewListButton extends StatelessWidget {
  const AddNewListButton();

  void _modalBottomSheetMenu(BuildContext context) {
    showModalBottomSheet(
      context: context,
      backgroundColor: Colors.transparent,
      isScrollControlled: true,
      builder: (builder) {
        return const ListScreenBottomSheetWidget();
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(5),
      ),
      elevation: 10,
      color: Theme.of(context).primaryColor,
      onPressed: () => _modalBottomSheetMenu(context),
      child: Text(
        '+ Add List',
        style: TextStyle(
          color: Colors.white,
          fontSize: SizeConfig.smallDevice ? 10 : 15,
        ),
      ),
    );
  }
}

Модальный нижний лист:

import 'package:flutter/material.dart';
import 'package:todo_spicotech/helpers/size_config.dart';

class ListScreenBottomSheetWidget extends StatelessWidget {
  const ListScreenBottomSheetWidget();
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        FocusScopeNode currentFocus = FocusScope.of(context);
        if (!currentFocus.hasPrimaryFocus) {
          currentFocus.unfocus();
        }
        currentFocus.unfocus();
      },
      child: Container(
        margin: const EdgeInsets.all(20.0),
        padding: EdgeInsets.only(
          bottom: MediaQuery.of(context).viewInsets.bottom,
        ),
        child: Material(
          borderRadius: BorderRadius.all(Radius.circular(15)),
          elevation: 10,
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[

                SizeConfig.smallDevice
                    ? Text(
                        'Create a new List',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 20,
                          color: Colors.grey[700],
                        ),
                      )
                    : Text(
                        'Create a new List',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          fontSize: 25,
                          color: Colors.grey[700],
                        ),
                      ),
                SizeConfig.smallDevice
                    ? const SizedBox(
                  height: 20,
                )
                    : const SizedBox(
                  height: 30,
                ),
                const ListTitleInputTextFieldWidget(),
                SizeConfig.smallDevice
                    ? const SizedBox(
                  height: 20,
                )
                    : const SizedBox(
                  height: 30,
                ),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    InkWell(
                      borderRadius: BorderRadius.circular(5),
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                      child: Ink(
                        padding: EdgeInsets.all(10),
                        child: const Text('CANCEL'),
                      ),
                    ),
                    RaisedButton(
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(5),
                      ),
                      elevation: 10,
                      color: Theme.of(context).primaryColor,
                      onPressed: () {},
                      child: Text(
                        'Create',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: SizeConfig.smallDevice ? 10 : 15,
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class ListTitleInputTextFieldWidget extends StatefulWidget {
  const ListTitleInputTextFieldWidget();
  @override
  _ListTitleInputTextFieldWidgetState createState() => _ListTitleInputTextFieldWidgetState();
}

class _ListTitleInputTextFieldWidgetState extends State<ListTitleInputTextFieldWidget> {
  @override
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: const InputDecoration(
        enabledBorder: OutlineInputBorder(
          borderSide: BorderSide(
            color: Colors.lightBlue,
          ),
        ),
        focusedBorder: OutlineInputBorder(
          borderSide: BorderSide(
            color: Colors.lightBlue,
          ),
        ),
        labelText: 'List Title',
        contentPadding: EdgeInsets.all(10),
      ),
    );
  }
}


person Hassan Hammad    schedule 05.04.2020    source источник


Ответы (1)


когда вы вызываете showModalBottomSheet , он фактически использует Navigator внутри

return Navigator.of(context, rootNavigator: useRootNavigator).push(_ModalBottomSheetRoute<T>(
builder: builder,

исходный код showModalBottomSheet https://github.com/flutter/flutter/blob/17079f26b54c8517678699a0cefe5f7bfec67b3f/packages/flutter/lib/src/material/bottom_sheet.dart#L635

Ответ команды Flutter на вопрос Pages on Navigator stack rebuild when a new page is pushed https://github.com/flutter/flutter/issues/11655#issuecomment-348287396

Это работает по назначению. В общем, вы должны исходить из того, что все виджеты могут быть перестроены в любое время, а это в основном просто оптимизация.

В частности, маршруты будут перестроены, потому что их состояние навигатора изменилось, поэтому им может потребоваться обновить способ отрисовки кнопок возврата и т.п.

person chunhunghan    schedule 06.04.2020
comment
Я в замешательстве, это похоже на то, что главная производительность подведена. Прямо сейчас у меня есть только несколько виджетов на ListsScreen, которые перестраиваются, что, если у меня их целая куча с несколькими сетевыми вызовами? Каждый раз будет новый сетевой вызов и все загрузится заново. Как это вообще адекватно? И как тогда мне оптимизировать свое приложение? - person Hassan Hammad; 06.04.2020
comment
Это текущее поведение. для FutureBuilder вы можете сослаться на этот github.com/flutter/flutter/issues/11426# issuecomment-414047398 - person chunhunghan; 07.04.2020
comment
Если метод сборки вашего виджета содержит сетевой вызов. вы можете рассмотреть возможность изменения дизайна этой части. - person chunhunghan; 07.04.2020
comment
Хорошо, я проверил эту проблему и попытаюсь реализовать ее. Спасибо. - person Hassan Hammad; 07.04.2020
comment
что новенького? @HassanHammad это сработало? - person genericUser; 22.02.2021
comment
@genericUser Я давно не работал над этим, но я думаю, что вместо этого использовал обходной путь, изменив дизайн. - person Hassan Hammad; 04.07.2021