Flutter Navigator 2.0 Routing via the Navigator()

8.7k views Asked by At

I am currently trying to build a web app with the new Flutter web beta. The thing is to be able to have a history, handle the forward and back buttons in the browser and be able to handle user inputs into the URL the new Navigator 2.0 API is required (at least from what I understood).

There are only a few resources available currently, based on which I am trying to build my Navigator. The resources I used:

I managed to get the Back and forward button, as well as the history working. However I am struggling to be able to handle the page switches (in the Navigator()). In the example from John he manages the different sites in the 'page: ' array of the Navigator Widget (in the routeDelegater). It seemed strange to me but I tried it like this and it did not really work (Code further down).

In the 'page: []' I first tried to have booleans that trigger (like the show404) but that wasn't very clean so my next try was to get the current page as follows (in the array):

if(_currentPage.name == 'pages[0]') pageBuilds[0]

This kinda worked, as I was then able to type in the segment /matchit/0 and the correct page loaded, however for some reason the '/' route didn't work anymore and I got the error

Navigator.onGenerateRoute was null, but the route named "/" was referenced 

I then tried to use the 'ongenerateRoute' but that just threw a bunch of errors. I am kinda new so maybe I did something wrong there. But it seemed to me like this was not the correct approach. And this is where I am currently stuck at. I don't know what to try next and am hoping that some of you guys can help me out.

My Route-Delegater looks as follows (I will include my comments in the code aswell, maybe it helps some of you, who are also looking to understand the Navigator 2.0, understand what is going on):

/**
 * The RouteDelegate defines application specific behavious of how the router
 * learns about changes in the application state and how it responds to them. 
 * It listens to the RouteInformation Parser and the app state and builds the Navigator with
 * the current list of pages (immutable object used to set navigator's history stack).
 */

//ChangeNotifier for the listeners as they are handled there
//The popRoute is handled by 'PopNavigatorRouterDelegateMixin'
class RoutesDelegater extends RouterDelegate<RoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<RoutePath> {
  //This is the state of the navigator widget (in build method)
  GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>();
  //RoutesDelegater()  : navigatorKey = GlobalKey<NavigatorState>();
  MyPage _currentPage;
  bool show404 = false; //checks if we show the 404 page

  List<MyPage> pages = [
    MyPage('ProjektListe'),
    MyPage('StudiListe'),
    MyPage('PRView'),
  ];

  List<Page> pageBuilds = [
    MaterialPage(key: ValueKey('Unknown'), child: UnknownScreen()),
    MaterialPage(key: ValueKey('Homepage'), child: MyFirstHomepage()),
    MaterialPage(key: ValueKey('OtherScreen'), child: OtherScreen()),
  ];

  //currentConfiguration detects changes in the route information
  //It helps complete the browser history and (IMPORTANT) makes the browser back and forward buttons work

  RoutePath get currentConfiguration {
    if (show404) {
      return RoutePath.unknown();
    }
    if (_currentPage == null) return RoutePath.home();
    //if not 404 or homepage it is some other page
    return RoutePath.details(pages.indexOf(_currentPage));
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
        key: navigatorKey,
        pages: //List.of(pageBuilds),
            [
          //pageBuilds[1],
          if (show404)
            pageBuilds[0]
          else if (_currentPage != null)
            pageBuilds[1]
        ],
        onPopPage: (route, result) {
          if (!route.didPop(result)) {
            return false;
          }
          _currentPage = null;
          show404 = false;
          //we are using Changenotifier
          notifyListeners();
          return true;
        });
  }

  void _handleTapped(MyPage page) {
    _currentPage = page;
    notifyListeners();
  }

  @override
  Future<void> setNewRoutePath(RoutePath path) async {
    //Handle the unknown path
    if (path.isUnknown) {
      show404 = true;
      _currentPage = null;
      return;
    }

    if (path.isDetailPage) {
      //Check if Path id is valid
      if (path.id.isNegative || path.id > pages.length - 1) {
        show404 = true;
        return;
      }
      _currentPage = pages[path.id];
    } else {
      //homepage will be shown
      _currentPage = null;
    }

    show404 = false;
  }
}

My RoutingInformationParser looks like this:

/*
* The RouteInformationParser takes the RouteInformation from RouteInformationProvider and
* parses it into a user-defined data type.
*/

class MyRoutesInformationParser extends RouteInformationParser<RoutePath> {
  @override
  Future<RoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    //routeInformation is an object we get from the uri
    final uri = Uri.parse(routeInformation.location);
    // Handle '/' (Home Path)
    //Path segments are the segments seperated by /, if we don't have any we are on Home
    if (uri.pathSegments.length == 0) {
      return RoutePath.home();
    }

    //We have 2, as we have matchit/...
    if (uri.pathSegments.length == 2) {
      //If there is no 'matchit' in the first path segment the path is unknown
      if (uri.pathSegments.first != 'matchit') return RoutePath.unknown();
      //If we now have the correct first segment we can now handle the rest of the segment
      final remaining = uri.pathSegments.elementAt(1);
      final id = int.tryParse(remaining);
      //if it fails we return the unknown path
      if (id == null) return RoutePath.unknown();
      return RoutePath.details(id);
    }

    //Handling the unknown Path, e.g. user just typed anything in uri
    return RoutePath.unknown();
  }

  //THIS IS IMPORTANT: Here we restore the web history
  @override
  RouteInformation restoreRouteInformation(RoutePath path) {
    //Here we set the routeInformation which is used above, e.g. /404 for unknown page
    if (path.isUnknown) {
      return RouteInformation(location: '/404');
    }
    if (path.isHomePage) {
      return RouteInformation(location: '/');
    }
    //Any other page is handled here via /matchit/... <= the id of the path
    if (path.isDetailPage) {
      return RouteInformation(location: '/matchit/${path.id}');
    }
    //If none of the paths are hit
    return null;
  }
}

Then we also have my Data-type for the routing information:

class RoutePath{
  final int id;
  final bool isUnknown;

  RoutePath.home()
      : id = null,
        isUnknown = false;

  //Details means here that it is any other side than Home or unknown
  RoutePath.details(this.id) : isUnknown = false;

  RoutePath.unknown()
      : id = null,
        isUnknown = true;

  //check if is on HomePage or other page, then either == null or != null
  //not needed for isInknown, as when unknown then = true as set above
  bool get isHomePage => id == null;
  bool get isDetailPage => id != null;
}

An lastly my Homepage() where the InformationParser and Delegater are initialized:

class Homepage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HomepageState();
}

class _HomepageState extends State<Homepage> {
  //initialize the RouteDelegater and Information Parser to be unsed further down

  RoutesDelegater _routesDelegater = RoutesDelegater();
  MyRoutesInformationParser _myRoutesInformationParser =
      MyRoutesInformationParser();

/*
 * Relevant routing information for this build method:
 * We need to use the MaterialApp.router else we can't use routerDelegate and routeInformationParser.
 * Then we define the delegate and Information Parser (they are initiated above)
 */

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
        title: 'MatchIT',
        routerDelegate: _routesDelegater,
        routeInformationParser: _myRoutesInformationParser,
        theme: ThemeData(primarySwatch: Colors.blue),
        debugShowCheckedModeBanner: false);
  }
}

Thank you in advance!

1

There are 1 answers

0
HenrikB On BEST ANSWER

I actually managed to solve it by adding another function that is called in my Navigator() in the RouteDelegator under pages[]:

 @override
  Widget build(BuildContext context) {
    return Navigator(
        key: navigatorKey,
        pages: [
          pageBuilds[1],
          if (show404)
            pageBuilds[0]
          else if (_currentPage != null)
            _getMyPage(_currentPage)
        ],
        onPopPage: (route, result) {
          if (!route.didPop(result)) {
            return false;
          }
          _currentPage = null;
          show404 = false;
          //we are using Changenotifier
          notifyListeners();
          return true;
        });
  }

  /*
   * This is where every other site than homepage and 404 is handled
   * to add another site to this application add the name to [List<MyPage> pages]
   * and then add the MaterialPage here
   */

  MaterialPage _getMyPage(MyPage currentPage) {
    if (currentPage.name == 'Login')
      return MaterialPage(key: ValueKey('LoginScreen'), child: OtherScreen());
    else
      return MaterialPage(
          key: ValueKey('ProfileScreen'), child: ResultScreen());
  }

I also changed the names in my Lists above (just so you guys can understand the code):

List<MyPage> pages = [
    MyPage('Login'),
    MyPage('Profile'),
  ];

  List<Page> pageBuilds = [
    MaterialPage(key: ValueKey('Unknown'), child: UnknownScreen()),
    MaterialPage(key: ValueKey('Homepage'), child: MyFirstHomepage()),
    MaterialPage(key: ValueKey('LoginScreen'), child: OtherScreen()),
    MaterialPage(key: ValueKey('ProfileScreen'), child: ResultScreen()),
  ];