[英]How do avoid markNeedsBuilder() error using Flutter_Riverpod and TextEditingControllers on TextFormFields?
下面的表格使用来自 flutter_riverpod package 的 ConsumerWidget 来观察 firebase stream 提供程序中名字/姓氏字段的更新。 然后使用 TextEditingControllers 在字段中设置watch
ed 文本值,并在更新 Firebase 中的帐户时获取文本值。
这一切都很好,直到我直接在 Firebase 中更改名字或姓氏字段中的值,这会导致 ui 中的重建。 虽然 UI 确实显示更新 Firebase 值,但我在运行日志中收到以下异常。
似乎riverpod 正在与state 上的TextEditingControllers 作战,这是有道理的,但是我该如何克服呢?
========基础库捕获的异常===================================== =============== 在为 TextEditingController 分派通知时引发以下断言:在构建期间调用 setState() 或 markNeedsBuild()。
无法将此表单小部件标记为需要构建,因为框架已经在构建小部件的过程中。 仅当其祖先之一当前正在构建时,小部件才能在构建阶段标记为需要构建。 这个例外是允许的,因为框架在子组件之前构建父窗口小部件,这意味着将始终构建脏后代。 否则,框架可能不会在此构建阶段访问此小部件。 调用 setState() 或 markNeedsBuild() 的小部件是:Form-[LabeledGlobalKey#78eaf] state:FormState#7d070 发出违规调用时当前正在构建的小部件是:FirstLastName 脏依赖项:[UncontrolledProviderScope]
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();
});
为 TextEditingController 发送通知时抛出了断言:在构建期间调用了 setState() 或 markNeedsBuild()。
当您在构建方法中更新 CahngeNotifier 时会显示此错误,在这种情况下,当您构建小部件时会更新 TextEditingController:
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
....
正如您所提到的, hooks_riverpod
可能是一种选择,但如果您不想在完全了解riverpod 或state 管理之前用图书馆淹没自己,我会推荐两种方法:
尝试使用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(),
),
);
}
}
另一种选择是使用 Riverpod 创建您自己的TextEditingController
,并在创建时使用 stream 的数据对其进行更新:
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;
然而,解决方案非常简单。 只需用零延迟的 Future 包装它:
Future.delayed(Duration.zero, (){
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
});
基本上,总是当你看到这个错误时,你需要在构建期间找到触发重建的代码并将其包装在 Future
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.