Как избежать ошибки markNeedsBuilder() при использовании Flutter_Riverpod и TextEditingControllers в TextFormFields?
В приведенной ниже форме используется ConsumerWidget из пакета flutter_riverpod для отслеживания обновлений в полях имени и фамилии в провайдере потока firebase. Затем, используя TextEditingControllers, я устанавливаю
watch
редактировать текстовые значения в полях, а также получать текстовые значения при обновлении учетной записи в 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();
});
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(),
);
}
}
Проблема в том, что вы запускаете перестройку своего виджета во время выполнения его метода сборки с помощью следующих строк:
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
Hovewer, решение довольно простое. Просто оберните его Future с нулевой задержкой:
Future.delayed(Duration.zero, (){
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
});
По сути, всегда, когда вы видите эту ошибку, вам нужно найти код, который запускает перестройку во время сборки, и обернуть его в Future