Search code examples
flutterflutter-webflutter-providerflutter-navigationflutter-state

Flutter web - Set state based on url


I am developing a flutter website with a persistent navigation bar on top that displays the current page the user is on, by adding a border around the title of the active page as shown below.

Navigation Bar

I am using url navigation on the website, so the user may be able to access different pages using the specific url:

  • webpage.com/#/about : Opens the 'About Me' page
  • webpage.com/#/skills : Opens the 'Skills' page and so on

Since the users can access a website page directly, I need to set the state of the navigation bar upon loading the page based on the url. But I am unable to call setState during build and I get the following error:

Another exception was thrown: setState() or markNeedsBuild() called during build.

I am using Navigators and Routes to take of the url navigation as follows:

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appName,
      theme: ThemeData(
        primarySwatch: Colors.cyan,
      ),
      builder: (context, child) => Dashboard(child: child!),
      navigatorKey: locator<NavigationService>().navigatorKey,
      onGenerateRoute: generateRoute,
      initialRoute: Routes.aboutRoute,
    );
  }
}

I am using Provider to control the state of the navigation bar as follows:

class NavigationBarProvider extends ChangeNotifier {
  List<bool> isSelected = [false, false, false, false];
  List<bool> isHovering = [false, false, false, false];

  void setSelected(int _index) {
    isSelected = List.filled(4, false);

    isSelected[_index] = true;

    notifyListeners();
  }

  void setHovering(int _index, bool _val) {
    isHovering[_index] = _val;

    notifyListeners();
  }

  bool isSelHov(int _index) {
    return isSelected[_index] || isHovering[_index];
  }
}

This is my widget for the Navigation Bar buttons:

return InkWell(
      onTap: () {
        locator<NavigationService>().navigateTo(routeName);

        provider.setSelected(index);
      },
      onHover: (value) {
        provider.setHovering(index, value);
      },
      child: SizedBox(
        width: 100.0,
        height: 50.0,
        child: Stack(
          children: [
            Center(
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 500),
                curve: Curves.easeInCirc,
                width: provider.isSelHov(index) ? 100.0 : 0.0,
                height: provider.isSelHov(index) ? 50.0 : 0.0,
                decoration: BoxDecoration(
                  border: Border.all(
                    color: Colors.white,
                    width: provider.isSelHov(index) ? 0.0 : 10.0,
                  ),
                  borderRadius: BorderRadius.circular(provider.isSelHov(index) ? 10.0 : 50.0),
                ),
              ),
            ),
            Center(
              child: Text(
                name,
                style: const TextStyle(
                  fontSize: 16.0,
                  color: Colors.white,
                ),
              ),
            ),
          ],
        ),
      ),
    );

Please help me understand how I may be able to go about this to set the state, and show the active Navigation Button when the website is launched using a specific url.

I appreciate any little help. Thank you very much.

EDIT

This is my code for the Navigation Bar, with the indexes:

ChangeNotifierProvider<NavigationBarProvider>(
            create: (context) => NavigationBarProvider(),
            child: Consumer<NavigationBarProvider>(
              builder: (context, provider, child) {
                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    NavBarButton(
                      index: 0,
                      name: 'About Me',
                      routeName: Routes.aboutRoute,
                      provider: provider,
                    ),
                    NavBarButton(
                      index: 1,
                      name: 'Projects',
                      routeName: Routes.projectsRoute,
                      provider: provider,
                    ),
                    NavBarButton(
                      index: 2,
                      name: 'Skills',
                      routeName: Routes.skillsRoute,
                      provider: provider,
                    ),
                    NavBarButton(
                      index: 3,
                      name: 'Contact Me',
                      routeName: Routes.contactsRoute,
                      provider: provider,
                    ),
                  ],
                );
              },
            ),
          )

EDIT 2

I managed to find a solution myself, and I have provided it below as one of the answers. Thank you.


Solution

  • I have managed to find a solution. I implemented Provider across the whole app, and changed the selected values when generating Routes as well. The following is my working code:

    My runApp:

    runApp(
        ChangeNotifierProvider<NavBarProvider>(
          create: (context) => NavBarProvider(),
          child: const MyApp(),
        ),
      );
    

    I am using a custom RouteRouteBuilder as follows:

    _FadeRoute(
          {required this.index, required this.child, required this.routeName})
          : super(
              settings: RouteSettings(name: routeName),
              pageBuilder: (
                BuildContext context,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
              ) {
                Future.delayed(Duration.zero, () async {
                  Provider.of<NavBarProvider>(context, listen: false).setSelected(index);
                });
                return child;
              },
              transitionsBuilder: (
                BuildContext context,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
                Widget child,
              ) {
                return FadeTransition(
                  opacity: animation,
                  child: child,
                );
              },
            );
    

    The rest of the code is the same, except I removed ChangeNotifierProvider as the parent in the NavBar Widget, and just left Consumer as follows:

    Consumer<NavigationBarProvider>(
              builder: (context, provider, child) {
                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    NavBarButton(
                      index: 0,
                      name: 'About Me',
                      routeName: Routes.aboutRoute,
                      provider: provider,
                    ),
                    NavBarButton(
                      index: 1,
                      name: 'Projects',
                      routeName: Routes.projectsRoute,
                      provider: provider,
                    ),
                    NavBarButton(
                      index: 2,
                      name: 'Skills',
                      routeName: Routes.skillsRoute,
                      provider: provider,
                    ),
                    NavBarButton(
                      index: 3,
                      name: 'Contact Me',
                      routeName: Routes.contactsRoute,
                      provider: provider,
                    ),
                  ],
                );
              },
            )