简体   繁体   English

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

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

The form below is using ConsumerWidget from the flutter_riverpod package to watch for updates on first/last name fields in a firebase stream provider.下面的表格使用来自 flutter_riverpod package 的 ConsumerWidget 来观察 firebase stream 提供程序中名字/姓氏字段的更新。 Then using TextEditingControllers I am both setting the watch ed text values in the fields and also getting the text values when I update the account in Firebase.然后使用 TextEditingControllers 在字段中设置watch ed 文本值,并在更新 Firebase 中的帐户时获取文本值。

This all works great until I change a value in the first or last name fields directly in Firebase, which causes a rebuild in the ui.这一切都很好,直到我直接在 Firebase 中更改名字或姓氏字段中的值,这会导致 ui 中的重建。 While the UI does display the update Firebase value I get the following Exception in the run logs.虽然 UI 确实显示更新 Firebase 值,但我在运行日志中收到以下异常。

Appears riverpod is battling with the TextEditingControllers over state, which makes sense, but how do I overcome this?似乎riverpod 正在与state 上的TextEditingControllers 作战,这是有道理的,但是我该如何克服呢?

======== Exception caught by foundation library ==================================================== The following assertion was thrown while dispatching notifications for TextEditingController: setState() or markNeedsBuild() called during build. ========基础库捕获的异常===================================== =============== 在为 TextEditingController 分派通知时引发以下断言:在构建期间调用 setState() 或 markNeedsBuild()。

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]调用 setState() 或 markNeedsBuild() 的小部件是:Form-[LabeledGlobalKey#78eaf] state:FormState#7d070 发出违规调用时当前正在构建的小部件是:FirstLastName 脏依赖项:[UncontrolledProviderScope]

Can I use the flutter_riverpod package when I am using a Stateful Widget that is required for using TextEditingControllers?当我使用使用 TextEditingControllers 所需的有状态小部件时,我可以使用flutter_riverpod package 吗? Or do I need to look at using the hooks_riverpod package or just riverpod package so that I can use TextEditingControllers to set values in fields and read values from fields?或者我是否需要查看使用hooks_riverpod package 或仅使用riverpod package 以便我可以使用 TextEditingControllers 在字段中设置值并从字段中读取值?

Code excerpts below:代码摘录如下:

account_setup.dart 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 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.为 TextEditingController 发送通知时抛出了断言:在构建期间调用了 setState() 或 markNeedsBuild()。

This error is shown when you update a CahngeNotifier inside a build method, in this case TextEditingController is updated when you're building the widgets:当您在构建方法中更新 CahngeNotifier 时会显示此错误,在这种情况下,当您构建小部件时会更新 TextEditingController:

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

As you mentioned, hooks_riverpod could be an option, but if you don't want to flood yourself with libraries until fully understand riverpod or state management I would recommend 2 approaches:正如您所提到的, hooks_riverpod可能是一种选择,但如果您不想在完全了解riverpod 或state 管理之前用图书馆淹没自己,我会推荐两种方法:

Try using ProviderListener (part of flutter_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,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Or you can use it inside FirstLastName and wrap the widget result, it should work the same (remember to delete the lines firstNameController.text = data.firstName; and lastNameController.text = data.lastName; inside when.data to prevent the error)或者您可以在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(),
      ),
    );
  }
}

The other option is to create your own TextEditingController using riverpod and update it with the data of the stream when its created:另一种选择是使用 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);
});

Then instead of creating them in the parent StatefulWidget just call it from the consumer in FirstLastName();然后不要在父 StatefulWidget 中创建它们,只需在FirstLastName(); (there is no need to pass TextEditingControllers in the constructor anymore) (不再需要在构造函数中传递 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(),
    );
  }
}

The problem is that you trigger rebuild of your widget during its build method execution with this lines:问题是您在其构建方法执行期间使用以下行触发了小部件的重建:

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

Hovewer, solution is quite simple.然而,解决方案非常简单。 Just wrap it with zero-delayed Future:只需用零延迟的 Future 包装它:

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

Basically, always when you see this error, you need to find the code that trigger rebuild during build and wrap it in Future基本上,总是当你看到这个错误时,你需要在构建期间找到触发重建的代码并将其包装在 Future

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 flutter_riverpod 如何监听列表 - How flutter_riverpod listens to a list 使用 flutter_riverpod 更新 StateProvider - update StateProvider with flutter_riverpod 为什么初始化期间flutter_riverpod出现state更改错误 - Why state change error occurs on flutter_riverpod during initialization 如何使用 flutter 中的 TextEditingControllers 进行计算? - How to calculate with TextEditingControllers in flutter? Flutter中的flutter_riverpod如何更方便的将ConsumerWidget转换为ConsumerStatefulWidget? - How does flutter_riverpod in Flutter convert ConsumerWidget to ConsumerStatefulWidget more conveniently? 当通知程序尝试在flutter_riverpod中更新其state错误时抛出异常 - Threw an exception when the notifier tried to update its state error in flutter_riverpod Flutter Riverpod,如何避免使用消费者重建整个页面 - Flutter Riverpod, how avoid rebuilding whole page using consumer Flutter_riverpod 安装时出现问题 - Flutter_riverpod giving me problems while installing flutter_riverpod - 在 `context` 可用时访问 `Ref` - flutter_riverpod - Accessing a `Ref` when `context` is available URI 的目标不存在:&#39;package:flutter_riverpod/flutter_riverpod.dart&#39; - Target of URI doesn't exist: 'package:flutter_riverpod/flutter_riverpod.dart'
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM