2

I've trying to create flutter apps using the new Navigation 2.0 with using MVVM pattern. Overall I got an issue when try to update my page from my starting Page that contain ChangeNotifierProvider this is how my code :

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"));
  }
}

my login_view_model.dart (just regular model that use ChangeNotifier to listen for value changes)

class LoginViewModel with ChangeNotifier {
  ...
}

The issue come when I got result as State.COMPLETED and call the onLoginSuccess method which reference to AppRouterDelegate function that later will call notifyListener() , and when that happen I got an error with these following message :

======== 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'
====================================================================================================

Does anyone know how to solve this issue ?

0 Answers0