Skip to main content

Command Palette

Search for a command to run...

Flutter Authentication Flow with Go Router and Provider

Splash, Onboarding and much more

Updated
13 min read
Flutter Authentication Flow with Go Router and Provider

Goal of this article

The goal of this article is to get a comprehensive understanding of the flutter declarative routing and the scenarios like login state, app initialization on startup, splash, onboarding, etc...

Problems that I'm trying to solve

assume we have a flutter application that we have to handle those situations.

  • We need to show the onboarding page when the user opens the app for the first time.
  • We need to execute some initialisation functions every time the app opens.
  • The application should be capable of kicking out the user anywhere in the application when the login state changes (log out).
  • And also app should be capable of handling the deep links regardless of login state.

omg.webp

I know, It looks like a lot. but stay with me I'm going to cover it all in this article

Starting the project

First of all, let's create the application. assuming that you have already installed flutter (I use the currently available latest version v2.8.0) Create a folder and inside it run this command,

flutter create --template app --overwrite.

I'm sure you are using vs-code or android studio for this so you don't have to create an app manually. if you do just ignore the above command and create the app from the IDE.

throughout the project, I'm going to run the application on the web. since all plugging, I'm using here is platform-independent you can use any platform you like it's totally up to you.

Okay, Let's add plugging to the pubspec.yaml under dependencies

  • provider: ^6.0.1

    we will be using the provider package to do all the state management in our application.

  • go_router: ^2.5.5

    The purpose of the go_router package is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handle deep and dynamic linking from Android, iOS and the web, along with several other navigation-related scenarios, while still (hopefully) providing an easy-to-use developer experience.

  • shared_preferences: ^2.0.11

    will be used to store some persistent data that we have kept in our application.

That's it, that's all the package we are going to use, run flutter pub get to get the packages.

flutter pub get

Let's start the coding

Open the main.dart file and remove everything and create the main function like this.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
  runApp(MyApp(sharedPreferences: sharedPreferences));
}

then create the MyApp class by extending the StatefulWidget because we need initState inside this widget.

class MyApp extends StatefulWidget {
  final SharedPreferences sharedPreferences;
  const MyApp({
    Key? key,
    required this.sharedPreferences,
  }) : super(key: key);

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

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return Container(

    );
  }
}

Okay, then let's create some files for the classes, take a close look and complete the appropriate files and folders for the next step.

flutter_01.PNG

Route Utils

Let's create enum class inside the route_utils.dart file that contain all the pages that our application has.

enum APP_PAGE {
  splash,
  login,
  home,
  error,
  onBoarding
}

Then we will add extension to the APP_PAGE for purpose convinent.

extension AppPageExtension on APP_PAGE {
  String get toPath {
    switch (this) {
      case APP_PAGE.home:
        return "/";
      case APP_PAGE.login:
        return "/login";
      case APP_PAGE.splash:
        return "/splash";
      case APP_PAGE.error:
        return "/error";
      case APP_PAGE.onBoarding:
        return "/start";
      default:
        return "/";
    }
  }

  String get toName {
    switch (this) {
      case APP_PAGE.home:
        return "HOME";
      case APP_PAGE.login:
        return "LOGIN";
      case APP_PAGE.splash:
        return "SPLASH";
      case APP_PAGE.error:
        return "ERROR";
      case APP_PAGE.onBoarding:
        return "START";
      default:
        return "HOME";
    }
  }

  String get toTitle {
    switch (this) {
      case APP_PAGE.home:
        return "My App";
      case APP_PAGE.login:
        return "My App Log In";
      case APP_PAGE.splash:
        return "My App Splash";
      case APP_PAGE.error:
        return "My App Error";
      case APP_PAGE.onBoarding:
        return "Welcome to My App";
      default:
        return "My App";
    }
  }
}

Next, we will create StatelessWidget inside every view file except splash_page.dart file, inside that create a StatefulWidget,

then, let's create the AppService class,

// ignore: non_constant_identifier_names
String LOGIN_KEY = "5FD6G46SDF4GD64F1VG9SD68";
// ignore: non_constant_identifier_names
String ONBOARD_KEY = "GD2G82CG9G82VDFGVD22DVG";

class AppService with ChangeNotifier {
  late final SharedPreferences sharedPreferences;
  final StreamController<bool> _loginStateChange = StreamController<bool>.broadcast();
  bool _loginState = false;
  bool _initialized = false;
  bool _onboarding = false;

  AppService(this.sharedPreferences);

  bool get loginState => _loginState;
  bool get initialized => _initialized;
  bool get onboarding => _onboarding;
  Stream<bool> get loginStateChange => _loginStateChange.stream;

  set loginState(bool state) {
    sharedPreferences.setBool(LOGIN_KEY, state);
    _loginState = state;
    _loginStateChange.add(state);
    notifyListeners();
  }

  set initialized(bool value) {
    _initialized = value;
    notifyListeners();
  }

  set onboarding(bool value) {
    sharedPreferences.setBool(ONBOARD_KEY, value);
    _onboarding = value;
    notifyListeners();
  }

  Future<void> onAppStart() async {
    _onboarding = sharedPreferences.getBool(ONBOARD_KEY) ?? false;
    _loginState = sharedPreferences.getBool(LOGIN_KEY) ?? false;

    // This is just to demonstrate the splash screen is working.
    // In real-life applications, it is not recommended to interrupt the user experience by doing such things.
    await Future.delayed(const Duration(seconds: 2));

    _initialized = true;
    notifyListeners();
  }
}

This is our AppService class, we create it with ChangeNotifier so we can listen for the state changes using Provider.

SharedPreferences instance that we created earlier will use in this class. and there are several values to keep track of the different states of the application.

While the initialized value only lives through the app life circle, loginState and onboarding values will be stored in local storage, because those values need to be kept even if the app is entirely closed. the onAppStart method is responsible for reading those values from local storage and finishing the app initialization. we will be showing the Splash screen until the onAppStart future is completed.

Ok, now we can define the routers, I'm going to do it by using Go Router if you're not familiar with its basic concepts first of all you should take a look at its documentation.

Let's start routing

hooray.webp

we need to create the AppRouter class which contains all the routing information in our application.

 class AppRouter {
  late final AppService appService;
  GoRouter get router => _goRouter;

  AppRouter(this.appService);

  late final GoRouter _goRouter = GoRouter(
    refreshListenable: appService,
    initialLocation: APP_PAGE.home.toPath,
    routes: <GoRoute>[
      GoRoute(
        path: APP_PAGE.home.toPath,
        name: APP_PAGE.home.toName,
        builder: (context, state) => const HomePage(),
      ),
      GoRoute(
        path: APP_PAGE.splash.toPath,
        name: APP_PAGE.splash.toName,
        builder: (context, state) => const SplashPage(),
      ),
      GoRoute(
        path: APP_PAGE.login.toPath,
        name: APP_PAGE.login.toName,
        builder: (context, state) => const LogInPage(),
      ),
      GoRoute(
        path: APP_PAGE.onBoarding.toPath,
        name: APP_PAGE.onBoarding.toName,
        builder: (context, state) => const OnBoardingPage(),
      ),
      GoRoute(
        path: APP_PAGE.error.toPath,
        name: APP_PAGE.error.toName,
        builder: (context, state) => ErrorPage(error: state.extra.toString()),
      ),
    ],
    errorBuilder: (context, state) => ErrorPage(error: state.error.toString()),
    redirect: (state) {
      final loginLocation = state.namedLocation(APP_PAGE.login.toPath);
      final homeLocation = state.namedLocation(APP_PAGE.home.toPath);
      final splashLocation = state.namedLocation(APP_PAGE.splash.toPath);
      final onboardLocation = state.namedLocation(APP_PAGE.onBoarding.toPath);

      final isLogedIn = appService.loginState;
      final isInitialized = appService.initialized;
      final isOnboarded = appService.onboarding;

      final isGoingToLogin = state.subloc == loginLocation;
      final isGoingToInit = state.subloc == splashLocation;
      final isGoingToOnboard = state.subloc == onboardLocation;

      // If not Initialized and not going to Initialized redirect to Splash
      if (!isInitialized && !isGoingToInit) {
        return splashLocation;
      // If not onboard and not going to onboard redirect to OnBoarding
      } else if (isInitialized && !isOnboarded && !isGoingToOnboard) {
        return onboardLocation;
      // If not logedin and not going to login redirect to Login
      } else if (isInitialized && isOnboarded && !isLogedIn && !isGoingToLogin) {
        return loginLocation;
      // If all the scenarios are cleared but still going to any of that screen redirect to Home
      } else if ((isLogedIn && isGoingToLogin) || (isInitialized && isGoingToInit) || (isOnboarded && isGoingToOnboard)) {
        return homeLocation;
      } else {
      // Else Don't do anything
        return null;
      }
    },
  );

Whoa, whoa what the fudge is going on here?

lay-down.webp

It looks like too much information to take but don't worry I'm going to break down it for you.

late final AppService appService;
GoRouter get router => _goRouter;

Here we define the AppService object and the GoRouter object, Since the AppService is listenable we can listen for the state changes and rebuild the router delegate according to the app states.

refreshListenable: appService,
initialLocation: APP_PAGE.home.toPath,

refreshListenable is that we define a listenable class to go router to react to changes. and the initialLocation is the default location that the app will redirect when there is no other path when the app starts (like deep links and dynamic links).

GoRoute(
  path: APP_PAGE.home.toPath,
  name: APP_PAGE.home.toName,
  builder: (context, state) => const HomePage(),
),

Here are all the predefined locations in our application. you can see that earlier we create APP_PAGE enum class and an extension for it. it is super convenient to define the path, and name when we have such a class that's why we created it.

If you take a closer look you will see we use ErrorPage on two different locations routers and errorBuilder. one inside the routers, we will use for manually navigate to the error page when there is a critical error in our application. the errorBuilder will use by Go Router to throw exceptions when there is something like an undefined navigation path.

one thing that I forgot to mention, there is a String argument on the error page called error which we will use to show an error to the user. when errorBuilder is triggered, always exception will be stored in the state.error object, when we navigate to the error page manually we will carry the error string inside the state.extra object. more information about it here.

Redirects

This method is the most important part of our application. it will handle all the core routing logic of the application, let's break down the block by block for better understanding.

final loginLocation = state.namedLocation(APP_PAGE.login.toName);
final homeLocation = state.namedLocation(APP_PAGE.home.toName);
final splashLocation = state.namedLocation(APP_PAGE.splash.toName);
final onboardLocation = state.namedLocation(APP_PAGE.onBoarding.toName);

Here we get all the locations from the router so we know where to redirect in different scenarios.

final isLogedIn = appService.loginState;
final isInitialized = appService.initialized;
final isOnboarded = appService.onboarding;

Then we get all the current states in our application from appService so we know what to do next when we redirect.

final isGoingToLogin = state.subloc == loginLocation;
final isGoingToInit = state.subloc == splashLocation;
final isGoingToOnboard = state.subloc == onboardLocation;

Finally we get where we actually go so we know when to redirect or not.

See it's not that complicated

complicated.webp

Ok, ok, the next part is quite complicated, let's break it down,

We know that there are two cases where the redirect method called

  • When we navigate to another location.
  • When appService object changes.

    because the router is always listing for its changes when it happens router will be rebuilt.

assume the app is starting for the first time, the sub-location of the router will be initialLocation which is home /, so this is the first case that I mentioned above the redirect method will be called, after collecting all the information we are in the if statement. so it will check app is initialized or not, obviously it is false. because we are stating the app and also we are not going to splash screen. so the statement will be valid we will return Splash Screen Location /splash. when we do that unintentionally also triggered the first case because the navigator location has changed. so the redirect method will be called again but this time isInitialized is still false but isGoingToInit is true because we are going to splash the screen. so along with the if statement all other conditions will be false. we just let go of the navigator to do its thing without changing anything. so at this point,

there is a three-state value of this application, and any of that is not changed yet.

app is just going to the splash screen

and already redirect method is called 3 times

those are the main three-point I need you to understand here. if you didn't please repeat the above paragraph again until you do.

before we start the splash screen we have to do one thing,

Adding Provider and Router to the App

class _MyAppState extends State<MyApp> {
  late AppService appService;

  @override
  void initState() {
    appService = AppService(widget.sharedPreferences);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AppService>(create: (_) => appService),
        Provider<AppRouter>(create: (_) => AppRouter(appService)),
      ],
      child: Builder(
        builder: (context) {
          final GoRouter goRouter = Provider.of<AppRouter>(context, listen: false).router;
          return MaterialApp.router(
            title: "Router App",
            routeInformationParser: goRouter.routeInformationParser,
            routerDelegate: goRouter.routerDelegate,
          );
        },
      ),
    );
  }
}

You can see in the above code we have created appService object and initialized it inside initState function with sharedPreferences instance. After that we have to wrap MaterialApp with MultiProvider and then add appService as ChangeNotifierProvider also we will declare AppRouter with appService as a provider too. finally we can add routeInformationParser and routerDelegate to the MaterialApp.router by getting it from context. We have not created AuthService yet, it will be added later.

Okay, back to the splash screen

Splash Screen

There is one thing we have to do in the splash screen which is we need to call the onAppStart function.

class SplashPage extends StatefulWidget {
  const SplashPage({ Key? key }) : super(key: key);

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

class _SplashPageState extends State<SplashPage> {
  late AppService _appService;

  @override
  void initState() {
    _appService = Provider.of<AppService>(context, listen: false);
    onStartUp();
    super.initState();
  }

  void onStartUp() async {
    await _appService.onAppStart();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(APP_PAGE.splash.toTitle),
      ),
      body: const Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

once we are on the splash screen onAppStart function will be triggered, so it will read _onboarding and _loginState values from the locale storage and will change _initialized to true then will call notifyListeners as soon as that happens, the router will be rebuilt and will recall the redirect function.

oh god, that's too much "re" 😂

facepalm-seriously.gif

Okay, back to the redirect function

At this point isInitialized is true and other values will be false so obviously, it will redirect to the onboardLocation and inside the OnBoardingPage user will press the Done button and we will change _onboarding to true, it pretty much the same thing I think you can now understand the redirect function logics. Let's move on to the AuthService

Authentication Service

The reason why we are creating a separate AuthService class while tricking the login state inside the AppService, it will be so clean and understandable to implement authentication logic in a separate class and just expose the login state by an event stream. if you have experience with FirebaseAuth you will easily understand what I'm doing here,

class AuthService {
  final StreamController<bool> _onAuthStateChange = StreamController.broadcast();

  Stream<bool> get onAuthStateChange => _onAuthStateChange.stream;

  Future<bool> login() async {

    // This is just to demonstrate the login process time.
    // In real-life applications, it is not recommended to interrupt the user experience by doing such things.
    await Future.delayed(const Duration(seconds: 1));

    _onAuthStateChange.add(true);
    return true;
  }

  void logOut() {
    _onAuthStateChange.add(false);
  }
}

ok, now it's time to update the main.dart with AuthService we will declare it in the initState and add a listener to change the loginState.

class _MyAppState extends State<MyApp> {
  late AppService appService;
  late AuthService authService;
  late StreamSubscription<bool> authSubscription;

  @override
  void initState() {
    appService = AppService(widget.sharedPreferences);
    authService = AuthService();
    authSubscription = authService.onAuthStateChange.listen(onAuthStateChange);
    super.initState();
  }

  void onAuthStateChange(bool login) {
    appService.loginState = login;
  }

  @override
  void dispose() {
    authSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<AppService>(create: (_) => appService),
        Provider<AppRouter>(create: (_) => AppRouter(appService)),
        Provider<AuthService>(create: (_) => authService),
      ],
      child: Builder(
        builder: (context) {
          final GoRouter goRouter = Provider.of<AppRouter>(context, listen: false).router;
          return MaterialApp.router(
            title: "Router App",
            routeInformationParser: goRouter.routeInformationParser,
            routerDelegate: goRouter.routerDelegate,
          );
        },
      ),
    );
  }
}

Finally, we have finished almost all the coding now,

tired-sleepy.gif

we just need to add some buttons and run the application. I'll just add the home page code here the full source code will be available on GitHub.

class HomePage extends StatelessWidget {
  const HomePage({ Key? key }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final authService = Provider.of<AuthService>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(APP_PAGE.home.toTitle),
      ),
      body: Center(
        child: Column(
          children: [
            TextButton(
              onPressed: () {
                authService.logOut();
              },
              child: const Text(
                "Log out"
              ),
            ),
            TextButton(
              onPressed: () {
                GoRouter.of(context).goNamed(APP_PAGE.error.toName, extra: "Erro from Home");
              },
              child: const Text(
                "Show Error"
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Okay, let's run the application,

2021-12-22 23-37-14.gif

You will clearly see everything is working as expected.

five.webp

Okay, I'm going to wrap up the article here. since it is already too long, Handling Deep Link will be discussed in a separate article later. The link will be updated.

I know this is not the most advanced way to do it. But I think it is good, at what it is capable of. Feel free to share your thoughts in the comment section. Don't hesitate to ask questions I will help you. Don't forget to like, and share the article with your Flutter friends.

Github Repository

Happy coding.

byeeee.webp

R

Hey man, nice tutorial, I really like the way that you create the authentication process! And I'm trying to do something like this. I'm got this error when try to execute the project!

unknown route name: /start
'package:go_router/src/configuration.dart':
Failed assertion: line 266 pos 12: '_nameToPath.containsKey(name)'

and everything it's is like the tutorial! Do you known if the problem are caused by some different version or something like this?

R

I found the problem, I think this is something related to new version of GoRouter or something like this.

Before

final loginLocation = state.namedLocation(APP_PAGE.login.toPath);
final homeLocation = state.namedLocation(APP_PAGE.home.toPath);
final splashLocation = state.namedLocation(APP_PAGE.splash.toPath);
final onboardLocation = state.namedLocation(APP_PAGE.onBoarding.toPath);

After

final onBoardLocation = state.namedLocation(APP_PAGE.onBoarding.toName);
final splashLocation = state.namedLocation(APP_PAGE.splash.toName);
final loginLocation = state.namedLocation(APP_PAGE.login.toName);
final homeLocation = state.namedLocation(APP_PAGE.home.toName);

But now there's this exception.

======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for GoRouterDelegate:
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>
  dependencies: [UnmanagedRestorationScope]
  state: _RouterState<Object>#2674b
The widget which was currently being built when the offending call was made was: Builder
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:5042:9)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:5054:6)
#2      State.setState (package:flutter/src/widgets/framework.dart:1223:15)
#3      _RouterState._handleRouterDelegateNotification (package:flutter/src/widgets/router.dart:796:5)
#4      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:433:24)
#5      GoRouterDelegate._setCurrentConfiguration (package:go_router/src/delegate.dart:269:5)
#6      GoRouterDelegate.setNewRoutePath.<anonymous closure> (package:go_router/src/delegate.dart:231:18)
#7      SynchronousFuture.then (package:flutter/src/foundation/synchronous_future.dart:43:39)
#8      GoRouterDelegate.setNewRoutePath (package:go_router/src/delegate.dart:227:14)
#9      _RouterState._processParsedRouteInformation.<anonymous closure> (package:flutter/src/widgets/router.dart:758:34)
#10     SynchronousFuture.then (package:flutter/src/foundation/synchronous_future.dart:43:39)
#11     _RouterState._processRouteInformation (package:flutter/src/widgets/router.dart:750:8)
#12     _RouterState._handleRouteInformationProviderNotification (package:flutter/src/widgets/router.dart:767:5)
#13     ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:433:24)
#14     GoRouteInformationProvider.notifyListeners (package:go_router/src/information_provider.dart:134:11)
#15     ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:433:24)
#16     AppService.onAppStart (package:customers/services/app_service.dart:48:5)
#17     _SplashPageState.onStartUp (package:customers/pages/splash/page.dart:27:22)
#18     _SplashPageState.initState (package:customers/pages/splash/page.dart:21:5)
#19     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5611:55)
#20     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5456:5)
...     Normal element mounting (226 frames)
#246    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#247    MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6893:36)
#248    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6905:32)
...     Normal element mounting (487 frames)
#735    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:411:11)
...     Normal element mounting (7 frames)
#742    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#749    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#756    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:411:11)
...     Normal element mounting (7 frames)
#763    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#770    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#777    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:411:11)
...     Normal element mounting (7 frames)
#784    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#791    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#798    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (35 frames)
#833    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#834    Element.updateChild (package:flutter/src/widgets/framework.dart:3846:18)
#835    _RawViewElement._updateChild (package:flutter/src/widgets/view.dart:291:16)
#836    _RawViewElement.mount (package:flutter/src/widgets/view.dart:314:5)
...     Normal element mounting (7 frames)
#843    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#844    Element.updateChild (package:flutter/src/widgets/framework.dart:3846:18)
#845    RootElement._rebuild (package:flutter/src/widgets/binding.dart:1354:16)
#846    RootElement.mount (package:flutter/src/widgets/binding.dart:1323:5)
#847    RootWidget.attach.<anonymous closure> (package:flutter/src/widgets/binding.dart:1276:18)
#848    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2844:19)
#849    RootWidget.attach (package:flutter/src/widgets/binding.dart:1275:13)
#850    WidgetsBinding.attachToBuildOwner (package:flutter/src/widgets/binding.dart:1088:27)
#851    WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:1070:5)
#852    WidgetsBinding.scheduleAttachRootWidget.<anonymous closure> (package:flutter/src/widgets/binding.dart:1056:7)
#856    _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
(elided 3 frames from class _Timer and dart:async-patch)
The GoRouterDelegate sending notification was: Instance of 'GoRouterDelegate'
====================================================================================================

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

This _InheritedProviderScope<AppService?> 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: _InheritedProviderScope<AppService?>
  value: Instance of 'AppService'
  listening to value
The widget which was currently being built when the offending call was made was: Builder
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:5042:9)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:5054:6)
#2      _InheritedProviderScopeElement.markNeedsNotifyDependents (package:provider/src/inherited_provider.dart:577:5)
#3      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:433:24)
#4      AppService.onAppStart (package:customers/services/app_service.dart:48:5)
#5      _SplashPageState.onStartUp (package:customers/pages/splash/page.dart:27:22)
#6      _SplashPageState.initState (package:customers/pages/splash/page.dart:21:5)
#7      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5611:55)
#8      ComponentElement.mount (package:flutter/src/widgets/framework.dart:5456:5)
...     Normal element mounting (226 frames)
#234    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#235    MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6893:36)
#236    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6905:32)
...     Normal element mounting (487 frames)
#723    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:411:11)
...     Normal element mounting (7 frames)
#730    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#737    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#744    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:411:11)
...     Normal element mounting (7 frames)
#751    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#758    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#765    _InheritedProviderScopeElement.mount (package:provider/src/inherited_provider.dart:411:11)
...     Normal element mounting (7 frames)
#772    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (7 frames)
#779    _NestedHookElement.mount (package:nested/nested.dart:187:11)
...     Normal element mounting (7 frames)
#786    SingleChildWidgetElementMixin.mount (package:nested/nested.dart:222:11)
...     Normal element mounting (35 frames)
#821    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#822    Element.updateChild (package:flutter/src/widgets/framework.dart:3846:18)
#823    _RawViewElement._updateChild (package:flutter/src/widgets/view.dart:291:16)
#824    _RawViewElement.mount (package:flutter/src/widgets/view.dart:314:5)
...     Normal element mounting (7 frames)
#831    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#832    Element.updateChild (package:flutter/src/widgets/framework.dart:3846:18)
#833    RootElement._rebuild (package:flutter/src/widgets/binding.dart:1354:16)
#834    RootElement.mount (package:flutter/src/widgets/binding.dart:1323:5)
#835    RootWidget.attach.<anonymous closure> (package:flutter/src/widgets/binding.dart:1276:18)
#836    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2844:19)
#837    RootWidget.attach (package:flutter/src/widgets/binding.dart:1275:13)
#838    WidgetsBinding.attachToBuildOwner (package:flutter/src/widgets/binding.dart:1088:27)
#839    WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:1070:5)
#840    WidgetsBinding.scheduleAttachRootWidget.<anonymous closure> (package:flutter/src/widgets/binding.dart:1056:7)
#844    _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
(elided 3 frames from class _Timer and dart:async-patch)
The AppService sending notification was: Instance of 'AppService'
====================================================================================================

I'll keep looking for a solution, but if anyone knows and can help here in the comments, I'd be very grateful.

R

Hi Ishan. Thank you for your article. I have a question, and need some help from you about go_router. I am in situation that's i want to display a different statefulshell.indexedstack for every type of logged user example : normal user , admin, or super admin so there will be a different nav bar with a different branches if you can please help me or lead me to a solution i have done a try to distinct the branches but it's also only see the initial location and ignore the rest of the routes if there is any solution to change the initial location dynamicly gor every type of user.

V
Vasant2y ago

As I was going through docs for refreshListenable, it appears this is going to be depreciated. Reference: github/flutter/flutter/issues/116651

M

Hi, thanks for the tutorial. however I couldn't make it work. Cloned your repo and got some errors from the get go. I had to

flutter create .

and

flutter pub upgrade

Besides updating the dependencies in pubspec.yaml

  provider: ^6.0.4
  go_router: ^5.2.0
  shared_preferences: ^2.0.15

Also, I had to change the redirect definition to

redirect: (context, state) {

After all that I could finally run the app but only to find out it never leaves the splashScreen... All it shows it the CircularProgressIndicator forever...

In onAppStart() I commented out the line that waited for two seconds then I ran into an error I couldn't figure it out.

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

I googled the error but failed miserable. Any ideas?

1
D

Same here. I think the difference between Go_Router versions causes the issue. When I run sample project, it works as expected.(2.5.5) However, when I updated go_router version number to the latest (5.2.1), it shows progress indicator forever.

Any idea to solve or go around the issue?

V

Daewoom Kim

Remove routeDelegare and routeInformationParser, and just provide goRouter object to routerConfig prop of MaterialApp.router constructor, like this:

routerConfig: goRouter,

1
K
Khoshbin2y ago

Valerii Igumentsev Thank you, I was struggling with that

J

Hi Ishan, thank you for the article. I love the global state handling you implemented for the app, I'll wait for the article using deep links. Could you explain about this same project for handling multiple streams?

1
B
Buba3y ago

Hi Ishanga, thank you for the article. Newbie here :D. Where should I keep the logged in user data? Should I keep it in the AppService provider or in the AuthService? Sorry just trying to learn flutter and figure out what is going on.

1
I

It’s better to use AuthService for keep the user data. You probably need to make variable for it and change Stream also to that type. And add some function to save and read user data from storage.

1
B
Buba3y ago

Ishanga Vidusha Thank you so much.

E

Hi Ishan. Thank you for your article. I have a question, and need some help from you about go_router.

First I want to know if the refresh works perfectly on web with your implementation. ? I'm working on a web app, with flutter. When I do a browser refresh I'm redirected to login page, and the login does not work again.

1
I

GoRouter perfectly works on Flutter web. To keep the login state on browser refresh you should save the state in some persistent storage and get it on app start perhaps you can use splash screen for it. In real-world applications people use may local storage to store access tokens or something to validate the login state.

J

Hi Ishan, Thankyou for the article. It was very useful to me. One question i would like to ask though! You have passed the bloc or provider globally above materialApp here? What about when I need to pass the bloc inside separate routes or sub routes.

1
I

Sorry for the late reply. I think you can do it using the navigatorBuilder function in the GoRouter. you can get more information from go router documentation. https://gorouter.dev/navigator-builder