3

I want to implement a GoRouter based navigation with a fixed Scaffold and AppBar, but change the title of the AppBar dynamically based on the selected route.

I'm using GoRouter's ShellRoute to have a fixed Scaffold and AppBar and tried changing the title using a riverpod Provider:

final titleProvider = StateProvider((ref) => 'Title');

ShellRoute(
   builder: (BuildContext context, GoRouterState state, Widget child) {
       return Scaffold(
         body: child,
         appBar: CustomAppBar()
       );
   },
   routes: [
       GoRoute(
          path: DashboardScreenWeb.routeLocation,
          name: DashboardScreenWeb.routeName,
          builder: (context, state) {
             ref.read(titleProvider.state).state = DashboardScreenWeb.title;
             return const DashboardScreenWeb();
          },
       ),
       GoRoute(
          path: BusinessDataScreen.routeLocation,
          name: BusinessDataScreen.routeName,
          builder: (context, state) {
            ref.read(titleProvider.state).state = BusinessDataScreen.title;
            return const BusinessDataScreen();
          },
        ),
....

My CustomAppBar widget uses this provider like this:

class CustomAppBar extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    var title = ref.watch(titleProvider);
    return new AppBar(
      title: Text(title!)
    );
  }
}

However, I get a lot of exceptions, most likely because I'm changing the state of the provider at the wrong time. What can I do about it?

======== Exception caught by widgets library =======================================================
The following StateNotifierListenerError was thrown building Builder(dirty):
At least listener of the StateNotifier Instance of 'StateController<String>' threw an exception
when the notifier tried to update its state.

The exceptions thrown are:

setState() or markNeedsBuild() called during build.
This UncontrolledProviderScope 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:
  UncontrolledProviderScope
The widget which was currently being built when the offending call was made was:
krishnaacharyaa
  • 14,953
  • 4
  • 49
  • 88
schneida
  • 729
  • 3
  • 11
  • 37

2 Answers2

1

Use the state property state.location and pass the title to the AppBar

Go Router


final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

final router = GoRouter(
  initialLocation: '/',
  navigatorKey: _rootNavigatorKey,
  routes: [
    ShellRoute(
      navigatorKey: _shellNavigatorKey,
      pageBuilder: (context, state, child) {
        String title;
        switch (state.location) {              //  Using state.location to set title
          case '/':
            title = "Initial Screen";
            break;
          case '/home':
            title = "Home Screen";
            break;
          default:
            title = "Default Screen";
        }
        return NoTransitionPage(
            child: ScaffoldAppAndBottomBar(
          appTitle: title,                    //  pass title here
          child: child,
        ));
      },
      routes: [
        GoRoute(
          parentNavigatorKey: _shellNavigatorKey,
          path: '/home',
          name: 'Home Title',
          pageBuilder: (context, state) {
            return const NoTransitionPage(
              child: Scaffold(
                body: Center(
                  child: Text("Home"),
                ),
              ),
            );
          },
        ),
        GoRoute(
          path: '/',
          name: 'App Title',
          parentNavigatorKey: _shellNavigatorKey,
          pageBuilder: (context, state) {
            return const NoTransitionPage(
              child: Scaffold(
                body: Center(child: Text("Initial")),
              ),
            );
          },
        ),
      ],
    ),
  ],
);

Custom AppBar


class CustomAppBar extends StatelessWidget {
  Widget child;
  String? appTitle;
  CustomAppBar(
      {super.key, required this.child, required this.appTitle});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text(appTitle ?? "Default"),
      ),
      body: SafeArea(child: child),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.go('/home');
        },
        child: const Icon(Icons.home),
      ),
    );
  }
}

Output:

enter image description here

enter image description here

krishnaacharyaa
  • 14,953
  • 4
  • 49
  • 88
  • this works perfectly fine .but how do I keep the backstack ? I need to pop to previous screen when backPressed. if I use `pushNamed` instead of `go` ,it works..but the `state.location` remains the same value. – dev Aug 21 '23 at 08:41
-1

you should define your titleProvider like this:

final titleProvider = Provider<String>((ref) => 'Title');

and to update the provider:

GoRoute(
      path: BusinessDataScreen.routeLocation,
      name: BusinessDataScreen.routeName,
      builder: (context, state) {

        return ProviderScope(
    overrides: [
        titleProvider.overrideWithValue('youTitleHere')
       ]
      child: const BusinessDataScreen(),
        );
      },
    ),
john
  • 1,438
  • 8
  • 18
  • Unfortunately, this still produces exactly the same error. Note that I don't think there is any difference in the definition between `titleProvider` in your code and in my sample, the only difference I spotted was `titleProvider.notifier` vs `titleProvider.state`, but as I said, that's not changing anything. – schneida Nov 05 '22 at 13:20
  • there is nothing wrong with my code. whats wrong is the way you are updating your provider. your should be overriding the value upon navigating to another screen. something like: `titleProvider.overriderEWithValue(yourTitle)` – john Nov 05 '22 at 13:36
  • I didn't say your code was wrong, just that it's not working in my scenario. I understood that I should use `ref.read(titleProvider.notifier)....` instead of `ref.read(titleProvider.state)....` to update, which is what I tried unsuccessfully. My `titleProvider` doesn't have an `overrideWithValue(String)` or similar - do I need to define the provider differently to get this method? – schneida Nov 05 '22 at 14:25
  • Ok, now I don't get any exceptions, but the title just remains at the initial "Title" value and is not updated anymore. Regarding the `ProviderScope` - I already do have one like this `runApp(const ProviderScope(child: MyApp()));` – schneida Nov 05 '22 at 15:03
  • oh i see. its because your app bar is static. what you need to do is to update the title inside the onPressed method. but if the title is only available inside your go_router, another option is to create the StateNotifier subClass where you do the updating inside and set a listenter inside the build method of every page. once your new screen is built, the listener will trigger the stateNotifier and update the title. – john Nov 05 '22 at 15:30