首页 > 解决方案 > Flutter:Navigation 2.0 上的 ChangeNotifier 问题,页面内有嵌套的 ChangeProvider

问题描述

我正在尝试使用新的 Navigation 2.0 和 MVVM 模式创建颤振应用程序。总的来说,当我尝试从包含ChangeNotifierProvider以下代码的起始页面更新页面时遇到问题:

app_router_delegate.dart

import 'package:flutter/material.dart';
import 'package:back_office/app/pages/dashboard_view.dart';
import 'package:back_office/app/pages/login_view.dart';
import 'package:back_office/app/pages/unknown_view.dart';
import 'package:back_office/domain/entities/user_entity.dart';

class AppRouterDelegate extends RouterDelegate<AppRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
  final GlobalKey<NavigatorState> navigatorKey;
  String _jwtToken;
  bool show404 = false;

  AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();

  AppRoutePath get currentConfiguration => _jwtToken == null
      ? AppRoutePath.home()
      : AppRoutePath.dashboard(1);

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        if (show404)
          MaterialPage(key: ValueKey('LoginPage'), child: UnknownPage())
        else
          MaterialPage(
              key: ValueKey('LoginPage'),
              child: LoginPage(
                  title: 'Back Office', onLoginSuccess: _onLoginSuccess)),
        if (_jwtToken != null)
          MaterialPage(
              key: ValueKey('DashboardPage'),
              child: DashboardPage(_jwtToken)),
      ],
      onPopPage: (route, result) {
        if(!route.didPop(result)) {
          return false;
        }
        show404 = false;
        _jwtToken = null;
        print("onPopPage ... ");
        notifyListeners();
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(AppRoutePath path) async {
    if (path.isUnknown) {
      show404 = true;
      return;
    }

    show404 = false;
  }

  void _onLoginSuccess(UserEntity userEntity) {
    print("router delegate jwtToken : " + userEntity.jwtToken);
    _jwtToken = userEntity.jwtToken;
    notifyListeners();
  }
}

class AppRoutePath {
  final int id;
  final bool isUnknown;

  AppRoutePath.unknown()
      : id = null,
        isUnknown = true;

  AppRoutePath.home()
      : id = null,
        isUnknown = false;

  AppRoutePath.dashboard(this.id) : isUnknown = false;

  bool get isHomePage => id == null;
  bool get isDashboardPage => id != null;
}

login_page.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'package:back_office/app/viewmodels/login_view_model.dart';
import 'package:back_office/app/widgets/loading_widget.dart';
import 'package:back_office/app/widgets/response_error_widget.dart';
import 'package:back_office/domain/entities/user_entity.dart';
import 'package:back_office/domain/resource.dart';

import 'package:provider/provider.dart';

class LoginPage extends StatefulWidget {
  LoginPage({Key key, this.title, this.onLoginSuccess}) : super(key: key);

  final String title;
  final dynamic onLoginSuccess;

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final userDomainController = TextEditingController();
  final passwordController = TextEditingController();
  LoginViewModel mViewModel;

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<LoginViewModel>(
      create: (mContext) => LoginViewModel(),
      builder: (mContext, _) {
        print("login_view builder called\n");
        mViewModel = mContext.watch<LoginViewModel>();
        final userResponse = mViewModel.response;
        return Scaffold(
            appBar: AppBar(
              // Here we take the value from the LoginPage object that was created by
              // the App.build method, and use it to set our appbar title.
              title: Text(widget.title),
            ),
            body: (() {
              print("Status : ${userResponse.status} ${userResponse.message}");
              switch (userResponse.status) {
                case Status.INITIAL:
                  return loginForm();
                case Status.LOADING:
                  return LoadingWidget();
                  break;
                case Status.COMPLETED:
                  widget.onLoginSuccess(userResponse.data);
                  break;
                case Status.ERROR:
                  return ResponseErrorWidget(
                    userAction: _handleUserAction,
                    errorMessage: userResponse.message,
                  );
                  break;
              }
            }())

            );
      },
    );


  }

  Widget loginForm() {
    return Column(
      children: <Widget>[
        Align(
          child: Text("MB Back Office"),
        ),
        Spacer(flex: 2),
        Container(
          width: 512,
          margin: EdgeInsets.symmetric(horizontal: 16),
          decoration: BoxDecoration(
            color: Color.fromARGB(95, 125, 131, 166),
            borderRadius: BorderRadius.circular(10),
          ),
          child: Form(
            key: _formKey,
            child: Column(
              children: [
                Padding(
                  padding: EdgeInsets.fromLTRB(32, 18, 32, 14),
                  child: TextFormField(
                    controller: userDomainController,
                    decoration: InputDecoration(
                        filled: true,
                        fillColor: Colors.white60,
                        border: OutlineInputBorder(),
                        labelText: 'User Domain'),
                    validator: (input) {
                      if (input == null || input.isEmpty) {
                        return 'user domain cannot be empty';
                      }
                      return null;
                    },
                  ),
                ),
                Padding(
                  padding: EdgeInsets.fromLTRB(32, 0, 32, 8),
                  child: TextFormField(
                    obscureText: true,
                    controller: passwordController,
                    autofillHints: [AutofillHints.password],
                    decoration: InputDecoration(
                        filled: true,
                        fillColor: Colors.white60,
                        border: OutlineInputBorder(), labelText: 'Password'),
                    validator: (input) {
                      if (input == null || input.isEmpty) {
                        return 'password cannot be empty';
                      }
                      return null;
                    },
                  ),
                ),
                Padding(
                  padding: EdgeInsets.fromLTRB(32, 0, 32, 18),
                  child: ElevatedButton(
                    onPressed: () {
                      if (_formKey.currentState.validate()) {
                        var userEntity = UserEntity(
                            applicationId: 'MBANK',
                            userId: userDomainController.text,
                            password: passwordController.text);
                        mViewModel.requestLogin(userEntity);
                      }
                    },
                    child: Text("LOGIN"),
                  ),
                )
              ],
            ),
          ),
        ),
        Spacer(flex: 2),
        Padding(
          padding: EdgeInsets.symmetric(
            vertical: 16,
          ),
          child: Text("copyright 2021"),
        ),
      ],
    );
  }

  void _handleUserAction(String action) {
    print("user Action : $action");
    mViewModel.setResponse(Resource<UserEntity>.initial("return after error"));
  }
}

我的 login_view_model.dart (只是使用 ChangeNotifier 来监听值变化的常规模型)

class LoginViewModel with ChangeNotifier {
  ...
}

当我得到 State.COMPLETED 的结果并调用onLoginSuccess引用稍后将调用的 AppRouterDelegate 函数的方法时,问题就notifyListener()出现了,当发生这种情况时,我收到以下消息的错误:

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

This Router<Object> 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: Router<Object>
  state: _RouterState<Object>#5bd11
The widget which was currently being built when the offending call was made was: Builder
  dirty
  dependencies: [_InheritedProviderScope<LoginViewModel>]
When the exception was thrown, this was the stack: 
C:/b/s/w/ir/cache/builder/src/out/host_debug/dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 236:49      throw_
packages/flutter/src/widgets/framework.dart 4138:11                                                                            <fn>
packages/flutter/src/widgets/framework.dart 4152:14                                                                            markNeedsBuild
packages/flutter/src/widgets/framework.dart 1287:5                                                                             setState
packages/flutter/src/widgets/router.dart 670:5                                                                                 [_handleRouterDelegateNotification]
...
The AppRouterDelegate sending notification was: Instance of 'AppRouterDelegate'
====================================================================================================

有谁知道如何解决这个问题?

标签: flutterflutter-providerflutter-change-notifier

解决方案


推荐阅读