簡體   English   中英

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

[英]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]

當我使用使用 TextEditingControllers 所需的有狀態小部件時,我可以使用flutter_riverpod package 嗎? 或者我是否需要查看使用hooks_riverpod package 或僅使用riverpod package 以便我可以使用 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();
});

為 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM