如何在 TextFormFields 上使用 Flutter_Riverpod 和 TextEditingControllers 避免 markNeedsBuilder() 错误? 当我使用使用 TextEditingControllers 所需的有状态小部件时,我可以使用 flutter_riverpod 包吗?或者我是否需要考虑使用 hooks_riverpod 包或仅使用 riverpod 包,以便我可以使用 TextEditingControllers 在字段中设置值并从字段

问题描述

下面的表单使用 Flutter_riverpod 包中的 ConsumerWidget 来监视 firebase 流提供程序中名字/姓氏字段的更新。然后使用 TextEditingControllers 我在字段中设置 watched 文本值,并在更新 Firebase 帐户时获取文本值。

这一切都很好,直到我直接在 Firebase 中更改名字或姓氏字段中的值,这会导致 ui 中的重建。虽然 UI 确实显示更新 Firebase 值,但我在运行日志中收到以下异常。

似乎 riverpod 正在与 TextEditingControllers 对抗状态,这是有道理的,但我该如何克服这个问题?

======== 基础库捕获异常 ================================== ================== 为 TextEditingController 分派通知时抛出以下断言: 在构建期间调用 setState() 或 markNeedsBuild()。

无法将此表单小部件标记为需要构建,因为框架已在构建小部件的过程中。仅当其祖先之一当前正在构建时,才可以将小部件标记为需要在构建阶段构建。允许此异常是因为框架在子级之前构建父级小部件,这意味着将始终构建脏后代。否则,框架可能不会在此构建阶段访问此小部件。 调用 setState() 或 markNeedsBuild() 的小部件是:Form-[LabeledGlobalKey#78eaf] 状态:FormState#7d070 进行违规调用时当前正在构建的小部件是:FirstLastName 肮脏的 依赖项:[UncontrolledProviderScope]

当我使用使用 TextEditingControllers 所需的有状态小部件时,我可以使用 flutter_riverpod 包吗?或者我是否需要考虑使用 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,child: TextFormField(
                  controller: lastNameController,decoration: kInputStringFields.copyWith(
                    hintText: 'Last Name',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 或状态管理之前让自己淹没在库中,我会推荐两种方法:

尝试使用 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,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 并在创建时使用流的数据更新它:

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,);
  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,child: child: Consumer(
                  builder: (context,child) {
                     final lastNameController = watch(lastNameProvider); //call it here
                     return TextFormField(
                       controller: lastNameController,decoration: kInputStringFields.copyWith(
                         hintText: 'LAst Name',);
      },);
  }
}
,

问题是您在使用以下行的构建方法执行期间触发了小部件的重建:

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

但是,解决方案非常简单。只需用零延迟 Future 包装它:

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

基本上,每次看到这个错误时,都需要找到构建时触发rebuild的代码,并在Future中进行包装