Как избежать ошибки markNeedsBuilder () при использовании Flutter_Riverpod и TextEditingControllers в TextFormFields?

В приведенной ниже форме используется ConsumerWidget из пакета flutter_riverpod для отслеживания обновлений в полях имени и фамилии в провайдере потока firebase. Затем, используя TextEditingControllers, я устанавливаю текстовые значения watched в полях, а также получаю текстовые значения при обновлении учетной записи в Firebase.

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

Похоже, Riverpod борется с TextEditingControllers из-за состояния, что имеет смысл, но как мне это преодолеть?

======== Исключение, обнаруженное библиотекой Foundation ===================================== =============== Следующее утверждение было выдано при отправке уведомлений для TextEditingController: setState () или markNeedsBuild (), вызванные во время сборки.

Этот виджет формы не может быть помечен как требующий сборки, поскольку фреймворк уже находится в процессе создания виджетов. Виджет может быть помечен как требующий создания на этапе сборки, только если один из его предков в настоящее время строит. Это исключение разрешено, потому что фреймворк строит родительские виджеты раньше дочерних, что означает, что всегда будет строиться грязный потомок. В противном случае платформа может не посещать этот виджет на этапе сборки. Виджет, для которого был вызван setState () или markNeedsBuild (), был: Form- [LabeledGlobalKey # 78eaf] state: FormState # 7d070 Виджет, который в настоящее время создавался, когда был сделан оскорбительный вызов, был: FirstLastName грязные зависимости: [UncontrolledProviderScope]

Могу ли я использовать пакет flutter_riverpod, когда я использую виджет с отслеживанием состояния, который требуется для использования TextEditingControllers? Или мне нужно использовать пакет hooks_riverpod или просто riverpod, чтобы я мог использовать TextEditingControllers для установки значений в полях и чтения значений из полей?

Отрывки кода ниже:

account_setup.dart

class AccountSetup extends StatefulWidget {
  @override
  _AccountSetupState createState() => _AccountSetupState();
}

class _AccountSetupState extends State<AccountSetup> {
  final TextEditingController _firstNameController = TextEditingController();
  final TextEditingController _lastNameController = TextEditingController();

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _firstNameController.dispose();
    _lastNameController.dispose();
    super.dispose();
  }

  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: Colors.white,
        body: Form(
          key: _formKey,
          child: ListView(
            children: [
              AccountSettingsTitle(
                title: 'Account Setup',
              ),
              FirstLastName(_firstNameController, _lastNameController),
              SizedBox(
                height: 24.0,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class FirstLastName extends ConsumerWidget {
  FirstLastName(
    this.firstNameController,
    this.lastNameController,
  );
  final TextEditingController firstNameController;
  final TextEditingController lastNameController;

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final account = watch(accountStreamProvider);
    return account.when(
      data: (data) {
        firstNameController.text = data.firstName;
        lastNameController.text = data.lastName;
        return Column(
          children: [
            Center(
              child: Padding(
                padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
                child: TextFormField(
                  controller: firstNameController,
                  decoration: kInputStringFields.copyWith(
                    hintText: 'First Name',
                  ),
                  autocorrect: false,
                  validator: (String value) {
                    if (value.isEmpty) {
                      return 'Enter first name';
                    }

                    return null;
                  },
                ),
              ),
            ),
            SizedBox(
              height: 14.0,
            ),
            Center(
              child: Padding(
                padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
                child: TextFormField(
                  controller: lastNameController,
                  decoration: kInputStringFields.copyWith(
                    hintText: 'Last Name',
                  ),
                  autocorrect: false,
                  validator: (String value) {
                    if (value.isEmpty) {
                      return 'Enter last name';
                    }

                    return null;
                  },
                ),
              ),
            ),
          ],
        );
      },
      loading: () => Container(),
      error: (_, __) => Container(),
    );
  }
}

top_level_providers.dart

final accountStreamProvider = StreamProvider.autoDispose<Account>((ref) {
  final database = ref.watch(databaseProvider);
  return database != null ? database.accountStream() : const Stream.empty();
});

person Zelf    schedule 22.02.2021    source источник


Ответы (2)


assertion было выдано при отправке уведомлений для TextEditingController: setState () или markNeedsBuild (), вызванных во время сборки.

Эта ошибка отображается, когда вы обновляете CahngeNotifier внутри метода сборки, в этом случае TextEditingController обновляется, когда вы создаете виджеты:

firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
....

Как вы упомянули, hooks_riverpod может быть вариантом, но если вы не хотите наводнять себя библиотеками, пока полностью не поймете Riverpod или управление состоянием, я бы порекомендовал 2 подхода:

Попробуйте использовать ProviderListener (часть flutter_riverpod):

class AccountSetup extends StatefulWidget {
  @override
  _AccountSetupState createState() => _AccountSetupState();
}

class _AccountSetupState extends State<AccountSetup> {
  final TextEditingController _firstNameController = TextEditingController();
  final TextEditingController _lastNameController = TextEditingController();

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _firstNameController.dispose();
    _lastNameController.dispose();
    super.dispose();
  }

  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: Colors.white,
        body: Form(
          key: _formKey,
          child: ListView(
            children: [
              AccountSettingsTitle(
                title: 'Account Setup',
              ),
              ProviderListener<AsyncValue>(
                provider: accountStreamProvider,
                onChange: (context, account) { //This will called when accountStreamProvider updates and a frame after the widget rebuilt
                  if(account is AsyncData) {
                    firstNameController.text = data.firstName;
                    lastNameController.text = data.lastName;
                  }
                },
                child: FirstLastName(_firstNameController, _lastNameController),
              ),
              SizedBox(
                height: 24.0,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Или вы можете использовать его внутри FirstLastName и обернуть результат виджета, он должен работать так же (не забудьте удалить строки firstNameController.text = data.firstName; и lastNameController.text = data.lastName; внутри when.data, чтобы предотвратить ошибку)

@override
  Widget build(BuildContext context, ScopedReader watch) {
    final account = watch(accountStreamProvider);
    return ProviderListener<AsyncValue>(
      provider: accountStreamProvider,
      onChange: (context, account) { //This will called when accountStreamProvider updates and a frame after the widget rebuilt
        if(account is AsyncData) {
           firstNameController.text = data.firstName;
           lastNameController.text = data.lastName;
        }
      },
      child: account.maybeWhen(
        data: (data) {
          /// don't call firstNameController.text = data.firstName here
          return Column(
             children: [
                ....
             ],
          );
        },
        orElse: () => Container(),
      ),
    );
  }
}

Другой вариант - создать свой собственный TextEditingController с помощью riverpod и обновить его данными потока при его создании:

final firstNameProvider = ChangeNotifierProvider.autoDispose<TextEditingController>((ref) {
  final account = ref.watch(accountStreamProvider);
  final String name = account.maybeWhen(
     data: (data) => data?.firstName,
     orElse: () => null,
  );
  return TextEditingController(text: name);
});

final lastNameProvider = ChangeNotifierProvider.autoDispose<TextEditingController>((ref) {
  final account = ref.watch(accountStreamProvider);
  final String lastName = account.maybeWhen(
     data: (data) => data?.lastName,
     orElse: () => null,
  );
  return TextEditingController(text: lastName);
});

Затем вместо того, чтобы создавать их в родительском StatefulWidget, просто вызовите его у потребителя в FirstLastName(); (больше нет необходимости передавать TextEditingControllers в конструкторе)

class FirstLastName extends ConsumerWidget {
  const FirstLastName({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final account = watch(accountStreamProvider);
    return account.maybeWhen(
      data: (data) {
        return Column(
          children: [
            Center(
              child: Padding(
                padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
                child: Consumer(
                  builder: (context, watch, child) {
                     final firstNameController = watch(firstNameProvider); //call it here
                     return TextFormField(
                       controller: firstNameController,
                       decoration: kInputStringFields.copyWith(
                         hintText: 'First Name',
                       ),
                       autocorrect: false,
                       validator: (String value) {
                         if (value.isEmpty) {
                          return 'Enter first name';
                         }
                         return null;
                       },
                    );
                  }
                ),
              ),
            ),
            SizedBox(
              height: 14.0,
            ),
            Center(
              child: Padding(
                padding: EdgeInsets.only(top: 10.0, left: 24.0, right: 24.0),
                child: child: Consumer(
                  builder: (context, watch, child) {
                     final lastNameController = watch(lastNameProvider); //call it here
                     return TextFormField(
                       controller: lastNameController ,
                       decoration: kInputStringFields.copyWith(
                         hintText: 'LAst Name',
                       ),
                       autocorrect: false,
                       validator: (String value) {
                         if (value.isEmpty) {
                          return 'Enter first name';
                         }
                         return null;
                       },
                    );
                  }
                ),
              ),
            ),
          ],
        );
      },
      orElse: () => Container(),
    );
  }
}
person EdwynZN    schedule 22.02.2021
comment
если (аккаунт - AsyncData) {firstNameController.text = data.firstName; lastNameController.text = data.lastName; } данные не являются параметром, как мне получить доступ к данным потока? - person Zelf; 23.02.2021
comment
Моя плохая, вместо data.firstName попробуйте account.data?.value?.firstName - person EdwynZN; 23.02.2021

Проблема в том, что вы запускаете перестройку своего виджета во время выполнения его метода сборки с помощью следующих строк:

firstNameController.text = data.firstName;
lastNameController.text = data.lastName;

Hovewer, решение довольно простое. Просто оберните его Future с нулевой задержкой:

Future.delayed(Duration.zero, (){
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
});

По сути, всегда, когда вы видите эту ошибку, вам нужно найти код, который запускает перестройку во время сборки, и обернуть его в Future

person Szymon Kowaliński    schedule 22.02.2021
comment
Это правильный ответ, и его можно легко исправить. Спасибо. Однако я дал ответ на вышеуказанное решение просто потому, что там много отличных вариантов. Ваш ответ тоже отлично работает и имеет смысл. - person Zelf; 23.02.2021