Search code examples
flutterflutter-webflutter-navigationflutter-change-notifier

RouterDelegate won't rebuild upon changes from listener Flutter


I'm trying to switch to Navigator 2.0 following the example at https://github.com/carloshwa/flutter-example/tree/navigator_2 adapting it to my code.

As in the example, I have a class AppState extends ChangeNotifier which RouterDelegate listens to

AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
    appState.addListener(notifyListeners);
    print('appState.addListener(notifyListeners) called');
  }

to build the correct page

pages: [MaterialPage(child: WebPageMainLayout(appState: appState,))],

where WebPageMainLayout adds a ConstrainedBox with WebsitePageDisplay as its child.

WebsitePageDisplay has a Column.

First Column's widget is NavigationBar from which buttons AppState's selectedPage gets set

void _handlePageTapped(String selected) {
    print('selected tapped is $selected');
    appState.selectedPage = selected;
  }

...

NavigationBarButton(
                title: AppLocalizations.instance.text('For retailers'),
                navigationPath: RetailersLandingRoute,
                onPressed: () {},
                onTapped: _handlePageTapped,
              ),

So once set it notifies his listeners (RouterDelegate).

The second Column's widget is the actual page to show returned from AppState's selectedWidget getter.

return Column(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        NavigationBar(),
        Expanded(
          child: widget.appState.selectedWidget,
        )
      ],
    );

The problem is that when a button is pressed the second widget (from AppState's getter) is not swapped, dough I get the correct prints from _handlePageTapped (eg. `/retailers from the above code)

I'm not sure if it's the RouterDelegate that dough, it gets notified of the changes in AppState, doesn't rebuild the Navigator with the new page, or I'm just implementing a wrong logic as I'm new to ChangeNotifier and Navigator 2.0 seems a bit over-complicated.

When the page first loads up I see the set of prints I setup from parseRouteInformation, currentConfiguration, restoreRouteInformation, setNewRoutePath' first with no value ( as I don't have a /route ) but then with the correct/cyclists` value.

AppRouteInformationParser.parseRouteInformation called for /
AppRouteInformationParser uri is /
AppRouteInformationParser.urlSegment switch: /
RouterDelegate.currentConfiguration appState.selectedPage is 
AppRouteInformationParser.restoreRouteInformation called for configuration 
RouterDelegate.setNewRoutePath configuration is Configuration {unknown: false, selectedPage: /cyclists}
AppState setting selectedPage to /cyclists
RouterDelegate.currentConfiguration appState.selectedPage is /cyclists
AppRouteInformationParser.restoreRouteInformation called for configuration /cyclists
restoreRouteInformation RouteInformation.location: /cyclists
RouterDelegate.currentConfiguration appState.selectedPage is /cyclists
AppRouteInformationParser.restoreRouteInformation called for configuration /cyclists
restoreRouteInformation RouteInformation.location: /cyclists
RouterDelegate.currentConfiguration appState.selectedPage is /cyclists
AppRouteInformationParser.restoreRouteInformation called for configuration /cyclists
restoreRouteInformation RouteInformation.location: /cyclists

but when I press a NavigationBar's button I only get the _handlePageTapped print. Also changing Url in chrome doesn't work.. Can you spot what I'm doing wrong?? As always many thanks for your help.

AppState

class AppState extends ChangeNotifier {
  String _selectedPage;
  AppState() : _selectedPage = '';

  String get selectedPage => _selectedPage;

  // works
  Widget get selectedWidget {
    switch (selectedPage) {
      case CyclistsLandingRoute:
        return CyclistLanding();
        break;
      case RetailersLandingRoute:
        return RetailerLanding();
        break;
      case MapRoute:
        return CityMap();
        break;
      case AboutRoute:
        return AboutUs();
        break;
      case TermsOfServiceRoute:
        return TermsOfService();
        break;
      case PrivacyPolicyRoute:
        return PrivacyPolicy();
        break;
      // case PrivacySettingsRoute:
      //   return PrivacyPolicySettings();
      //   break;
      case CommunityGuidelinesRoute:
        return CommunityGuidelines();
        break;
      case LegalNoticeRoute:
        return LegalNotice();
        break;
      default:
        return CyclistLanding();
    }
  }

  set selectedPage(String page) {
    print('AppState setting selectedPage to $page');
    _selectedPage = page;
    notifyListeners();
  }
}

RouterDelegate

class AppRouterDelegate extends RouterDelegate<Configuration>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<Configuration> {
  final GlobalKey<NavigatorState> navigatorKey;
  AppState appState = AppState();
  AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
    appState.addListener(notifyListeners);
    print('appState.addListener(notifyListeners) called');
  }


  @override
  Configuration get currentConfiguration {

    print(
        'RouterDelegate.currentConfiguration appState.selectedPage is ${appState.selectedPage}');
    switch (appState.selectedPage) {
      case CyclistsLandingRoute:
        return Configuration.cyclists(appState.selectedPage);
        break;
      case RetailersLandingRoute:
        return Configuration.retailers(appState.selectedPage);
        break;
      case MapRoute:
        return Configuration.map(appState.selectedPage);
        break;
      case AboutRoute:
        return Configuration.about(appState.selectedPage);
      case TermsOfServiceRoute:
        return Configuration.termsOfService(appState.selectedPage);
      case PrivacyPolicyRoute:
        return Configuration.privacyPolicy(appState.selectedPage);
        break;
      // case PrivacySettingsRoute:
      //   return Configuration.privacySettings();
      //   break;
      case CommunityGuidelinesRoute:
        return Configuration.communityGuidelines(appState.selectedPage);
        break;
      case LegalNoticeRoute:
        return Configuration.legalNotice(appState.selectedPage);
        break;
      default:
        return Configuration.cyclists(appState.selectedPage);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        MaterialPage(
            child: WebPageMainLayout(
          appState: appState,
        ))
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) return false;
        if (appState.selectedPage != null) {
          appState.selectedPage = null;
          notifyListeners();
        }

        // show404 = false;
        notifyListeners();
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(Configuration configuration) async {
    print(
        'RouterDelegate.setNewRoutePath configuration is ${configuration.toString()}');

    if (configuration.isUnknown) {
      // show404 = true;
      appState.selectedPage = null;
      notifyListeners();
      return;
    } else if (configuration.isCyclist) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isRetailer) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isMap) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isAbout) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isTermsOfService) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isPrivacyPolicy) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isPrivacySettings) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isCommunityGuidelines) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    } else if (configuration.isLegalNotice) {
      appState.selectedPage = configuration.selectedPage;
      notifyListeners();
      return;
    }
    return;
  }
}

Configuration

class Configuration {
  final bool unknown;
  final String selectedPage;

  Configuration.cyclists(this.selectedPage) : unknown = false;
  Configuration.retailers(this.selectedPage) : unknown = false;
  Configuration.map(this.selectedPage) : unknown = false;
  Configuration.about(this.selectedPage) : unknown = false;
  Configuration.termsOfService(this.selectedPage) : unknown = false;
  Configuration.privacyPolicy(this.selectedPage) : unknown = false;
  Configuration.privacySettings(this.selectedPage) : unknown = false;
  Configuration.communityGuidelines(this.selectedPage) : unknown = false;
  Configuration.legalNotice(this.selectedPage) : unknown = false;

  // bool get isHome => unknown == false;
  bool get isCyclist =>
      selectedPage == CyclistsLandingRoute; //unknown == false;
  bool get isRetailer =>
      selectedPage == RetailersLandingRoute; //unknown == false;
  bool get isMap => selectedPage == MapRoute; //unknown == true;
  bool get isAbout => selectedPage == AboutRoute; //unknown == false;
  bool get isTermsOfService =>
      selectedPage == TermsOfServiceRoute; //unknown == false;
  bool get isPrivacyPolicy =>
      selectedPage == PrivacyPolicyRoute; //unknown == false;
  bool get isPrivacySettings =>
      selectedPage == PrivacySettingsRoute; //unknown == false;
  bool get isCommunityGuidelines =>
      selectedPage == CommunityGuidelinesRoute; //unknown == false;
  bool get isLegalNotice =>
      selectedPage == LegalNoticeRoute; //unknown == false;
  bool get isUnknown => unknown == true;

  @override
  String toString() =>
      'Configuration {unknown: $unknown, selectedPage: $selectedPage}';
}

Parser

class AppRouteInformationParser extends RouteInformationParser<Configuration> {
  @override
  Future<Configuration> parseRouteInformation(
      RouteInformation routeInformation) async {
    print(
        'AppRouteInformationParser.parseRouteInformation called for ${routeInformation.location}');

    final Uri uri = Uri.parse(routeInformation.location);
    print('AppRouteInformationParser uri is $uri');

    switch (routeInformation.location) {
      case '/':
        print('AppRouteInformationParser.urlSegment switch: /');
        return Configuration.cyclists(CyclistsLandingRoute);
        break;
      case CyclistsLandingRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: cyclists');
        return Configuration.cyclists(CyclistsLandingRoute);
        break;
      case RetailersLandingRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: retailers');
        return Configuration.retailers(RetailersLandingRoute);
        break;
      case MapRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: map');
        return Configuration.map(MapRoute);
        break;
      case AboutRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: about');
        return Configuration.about(AboutRoute);
        break;
      case TermsOfServiceRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: terms-of-service');
        return Configuration.termsOfService(TermsOfServiceRoute);
        break;
      case PrivacyPolicyRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: privacy-policy');
        return Configuration.privacyPolicy(PrivacyPolicyRoute);
        break;
      case PrivacySettingsRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: privacy-settings');
        return Configuration.privacySettings(PrivacySettingsRoute);
        break;
      case CommunityGuidelinesRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: community-guidelines');
        return Configuration.communityGuidelines(CommunityGuidelinesRoute);
        break;
      case LegalNoticeRoute:
        print(
            'AppRouteInformationParser.routeInformation.location switch case: legal-notice');
        return Configuration.legalNotice(LegalNoticeRoute);
        break;

      default:
        print('AppRouteInformationParser.routeInformation.location: home');
        return Configuration.cyclists(CyclistsLandingRoute);
      // return Configuration.home();
    }
  }

  @override
  RouteInformation restoreRouteInformation(Configuration configuration) {

    print(
        'AppRouteInformationParser.restoreRouteInformation called for configuration ${configuration.selectedPage}');
    RouteInformation information = RouteInformation(location: '/cyclists');

    // if (configuration.isHome) {
    //   information = RouteInformation(location: '/');
    // } else
    if (configuration.isCyclist) {
      print('restoreRouteInformation RouteInformation.location: /cyclists');
      information = RouteInformation(location: '/cyclists');
    } else if (configuration.isRetailer) {
      print('restoreRouteInformation RouteInformation.location: /retailers');
      information = RouteInformation(location: '/retailers');
    } else if (configuration.isMap) {
      print('restoreRouteInformation RouteInformation.location: /map');
      information = RouteInformation(location: '/map');
    } else if (configuration.isAbout) {
      print('restoreRouteInformation RouteInformation.location: /about');
      information = RouteInformation(location: '/about');
    } else if (configuration.isTermsOfService) {
      print(
          'restoreRouteInformation RouteInformation.location: /terms-of-service');
      information = RouteInformation(location: '/terms-of-service');
    } else if (configuration.isPrivacyPolicy) {
      print(
          'restoreRouteInformation RouteInformation.location: /privacy-policy');
      information = RouteInformation(location: '/privacy-policy');
    } else if (configuration.isPrivacySettings) {
      print(
          'restoreRouteInformation RouteInformation.location: /privacy-settings');
      information = RouteInformation(location: '/privacy-settings');
    } else if (configuration.isCommunityGuidelines) {
      print(
          'restoreRouteInformation RouteInformation.location: /community-guidelines');
      information = RouteInformation(location: '/community-guidelines');
    } else if (configuration.isLegalNotice) {
      print('restoreRouteInformation RouteInformation.location: /legal-notice');
      information = RouteInformation(location: '/legal-notice');
    }
    // else if (configuration.isUnknown) {
    // information = RouteInformation(location: '/unknown');
    // }

    return information;
  }
}

Solution

  • Finally found out where I was mistaking.. I was instantiating a new AppState in some widgets (NavigationBar) instead of passing the one from RouterDelegate.. Once corrected, it works as expected. I guess that focussing on the new Navigator 2.0 system made my brain go novice lol.