如何在 TextFormFields 上使用 Flutter_Riverpod 和 TextEditingControllers 避免 markNeedsBuilder() 错误?

How do avoid markNeedsBuilder() error using Flutter_Riverpod and TextEditingControllers on TextFormFields?

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

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

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

======== Exception caught by foundation library ==================================================== The following assertion was thrown while dispatching notifications for TextEditingController: setState() or markNeedsBuild() called during build.

This Form widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase. The widget on which setState() or markNeedsBuild() was called was: Form-[LabeledGlobalKey#78eaf] state: FormState#7d070 The widget which was currently being built when the offending call was made was: FirstLastName dirty dependencies: [UncontrolledProviderScope]

我可以使用 flutter_riverpod package when I am using a Stateful Widget that is required for using TextEditingControllers? Or do I need to look at using the hooks_riverpod package or just 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();
});

assertion was thrown while dispatching notifications for TextEditingController: setState() or markNeedsBuild() called during build.

当您在构建方法中更新 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(),
      ),
    );
  }
}

另一种选择是使用 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,
     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