Giter VIP home page Giter VIP logo

tomgilder / routemaster Goto Github PK

View Code? Open in Web Editor NEW
329.0 13.0 59.0 984 KB

Easy-to-use Navigator 2.0 router for web, mobile and desktop. URL-based routing, simple navigation of tabs and nested routes.

Home Page: https://pub.dev/packages/routemaster

License: MIT License

Dart 67.73% Kotlin 0.11% Swift 1.00% Objective-C 0.19% HTML 2.65% Shell 0.15% Ruby 0.74% CMake 11.94% C++ 14.50% C 0.99%
flutter flutter-package flutter-routing flutter-router navigation

routemaster's Introduction

Routemaster

Hello! Routemaster is an easy-to-use router for Flutter, which wraps over Navigator 2.0... and has a silly name.

Build codecov pub

Features

  • Simple declarative mapping from URLs to pages
  • Easy-to-use API: just Routemaster.of(context).push('/page')
  • Really easy nested navigation support for tabs
  • Multiple route maps: for example one for a logged in user, another for logged out
  • Observers to easily listen to route changes
  • Covered by over 250 unit, widget and integration tests

Here's the entire routing setup needed for an app featuring tabs and pushed routes:

final routes = RouteMap(
  routes: {
    '/': (_) => CupertinoTabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),

    '/feed': (_) => MaterialPage(child: FeedPage()),
    '/settings': (_) => MaterialPage(child: SettingsPage()),
    '/feed/profile/:id': (info) => MaterialPage(
      child: ProfilePage(id: info.pathParameters['id'])
    ),
  }
);

void main() {
  runApp(
      MaterialApp.router(
        routerDelegate: RoutemasterDelegate(routesBuilder: (context) => routes),
        routeInformationParser: RoutemasterParser(),
      ),
  );
}

And then to navigate:

Routemaster.of(context).push('/feed/profile/1');

...you can see this in action in this simple app example.

There's also a more advanced example.

I would love any feedback you have! Please create an issue for API feedback.

Documentation

Begin with the quick start below, but also see the API reference, wiki and FAQs.


Quick start API tour

Overview

Routemaster generates pages based on the current path. This is the key concept of its path-base routing. Path structure matters.

It uses the path to decide where a page should be pushed. This means the path needs to match your intended page hierarchy.

For example:

'/tabs': (route) => TabPage(child: HomePage(), paths: ['one', 'two']),

// First tab default page
'/tabs/one': (route) => MaterialPage(child: TabOnePage()),

// Second tab default page
'/tabs/two': (route) => MaterialPage(child: TabTwoPage()),

// Second tab sub-page: will be displayed in the 2nd tab because it
// starts with '/tabs/two'
'/tabs/two/subpage': (route) => MaterialPage(child: TabTwoPage()),

// Not a tab page: will not be displayed in in a tab
// because the path doesn't start with '/tabs/one' or '/tabs/two'
'/tabs/notInATab': (route) => MaterialPage(child: NotTabPage()),

Any child paths that begin with /tabs/one or /tabs/two will be pushed into the correct tab.

When navigating to /tabs/two/subpage, the TabPage will be asked "hey, do you know how to handle this path?" and it'll go "sure! it starts with /tabs/two, so it goes in my second tab".

However, navigating to /tabs/notInATab will not be displayed in a tab, but pushed on top of the tab bar.

TabPage will be all "yeah sorry, no idea what to do with that, doesn't match any of my tab paths" and its parent will be asked to hande it.

Path hierarchy matters, for example changing where dialogs are displayed.

Routing

Basic app routing setup

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) => RouteMap(routes: {
      '/': (routeData) => MaterialPage(child: PageOne()),
      '/two': (routeData) => MaterialPage(child: PageTwo()),
    }),
  ),
  routeInformationParser: RoutemasterParser(),
)

Navigate from within pages

Routemaster.of(context).push('relative-path');
Routemaster.of(context).push('/absolute-path');

Routemaster.of(context).replace('relative-path');
Routemaster.of(context).replace('/absolute-path');

Path parameters

// Path '/products/123' will result in ProductPage(id: '123')
RouteMap(routes: {
  '/products/:id': (route) => MaterialPage(
        child: ProductPage(id: route.pathParameters['id']),
      ),
  '/products/myPage': (route) => MaterialPage(MyPage()),
})

Note that routes without path parameters have priority, so in the above example /products/myPage will show MyPage.

Query parameters

// Path '/search?query=hello' results in SearchPage(query: 'hello')
RouteMap(routes: {
  '/search': (route) => MaterialPage(
        child: SearchPage(query: route.queryParameters['query']),
      ),
})

Get current path info within a widget

RouteData.of(context).path; // Full path: '/product/123?query=param'
RouteData.of(context).pathParameters; // Map: {'id': '123'}
RouteData.of(context).queryParameters; // Map: {'query': 'param'}

Tabs

Setup:

RouteMap(
  routes: {
    '/': (route) => TabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),
    '/feed': (route) => MaterialPage(child: FeedPage()),
    '/settings': (route) => MaterialPage(child: SettingsPage()),
  },
)

Main page:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final tabPage = TabPage.of(context);

    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: tabPage.controller,
          tabs: [
            Tab(text: 'Feed'),
            Tab(text: 'Settings'),
          ],
        ),
      ),
      body: TabBarView(
        controller: tabPage.controller,
        children: [
          for (final stack in tabPage.stacks) PageStackNavigator(stack: stack),
        ],
      ),
    );
  }
}

Cupertino tabs

Setup:

RouteMap(
  routes: {
    '/': (route) => CupertinoTabPage(
          child: HomePage(),
          paths: ['/feed', '/settings'],
        ),
    '/feed': (route) => MaterialPage(child: FeedPage()),
    '/settings': (route) => MaterialPage(child: SettingsPage()),
  },
)

Main page:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final tabState = CupertinoTabPage.of(context);

    return CupertinoTabScaffold(
      controller: tabState.controller,
      tabBuilder: tabState.tabBuilder,
      tabBar: CupertinoTabBar(
        items: [
          BottomNavigationBarItem(
            label: 'Feed',
            icon: Icon(CupertinoIcons.list_bullet),
          ),
          BottomNavigationBarItem(
            label: 'Settings',
            icon: Icon(CupertinoIcons.search),
          ),
        ],
      ),
    );
  }
}

Guarded routes

Show default not found page if validation fails:

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : NotFound()

Redirect to another page if validation fails (changes URL):

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : Redirect('/no-access'),

Show another page if validation fails (doesn't change URL):

'/protected-route': (route) => 
    canUserAccessPage()
      ? MaterialPage(child: ProtectedPage())
      : MaterialPage(child: CustomNoAccessPage())

404 Page

Default page to shown on unknown URL:

RouteMap(
    onUnknownRoute: (route, context) {
        return MaterialPage(child: NotFoundPage());
    },
    routes: {
        '/': (_) => MaterialPage(child: HomePage()),
    },
)

Redirect

Redirect one route to another:

RouteMap(routes: {
    '/one': (routeData) => MaterialPage(child: PageOne()),
    '/two': (routeData) => Redirect('/one'),
})

Redirect all routes to login page, for a logged-out route map:

RouteMap(
  onUnknownRoute: (_) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
)

Passing path parameters from original to the redirect path:

RouteMap(routes: {
    '/user/:id': (routeData) => MaterialPage(child: UserPage(id: id)),
    '/profile/:uid': (routeData) => Redirect('/user/:uid'),
})

Swap routing map

You can swap the entire routing map at runtime.

This is particularly useful for different pages depending on whether the user is logged in:

final loggedOutMap = RouteMap(
  onUnknownRoute: (route, context) => Redirect('/'),
  routes: {
    '/': (_) => MaterialPage(child: LoginPage()),
  },
);

final loggedInMap = RouteMap(
  routes: {
    // Regular app routes
  },
);

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    routesBuilder: (context) {
			// This will rebuild when AppState changes
      final appState = Provider.of<AppState>(context);
      return appState.isLoggedIn ? loggedInMap : loggedOutMap;
    },
  ),
  routeInformationParser: RoutemasterParser(),
);

Navigation observers

class MyObserver extends RoutemasterObserver {
	// RoutemasterObserver extends NavigatorObserver and
	// receives all nested Navigator events
  @override
  void didPop(Route route, Route? previousRoute) {
    print('Popped a route');
  }

	// Routemaster-specific observer method
  @override
  void didChangeRoute(RouteData routeData, Page page) {
    print('New route: ${routeData.path}');
  }
}

MaterialApp.router(
  routerDelegate: RoutemasterDelegate(
    observers: [MyObserver()],
    routesBuilder: (_) => routeMap,
  ),
  routeInformationParser: RoutemasterParser(),
);

Navigate without a context

app.dart

final routemaster = RoutemasterDelegate(
  routesBuilder: (context) => routeMap,
);

MaterialApp.router(
  routerDelegate: routemaster,
  routeInformationParser: RoutemasterParser(),
)

my_widget.dart

import 'app.dart';

void onTap() {
  routemaster.push('/blah');
}

Hero animations

Hero animations will work automatically on the top-level navigator (assuming you're using MaterialApp or CupertinoApp).

For any child navigators, you'll need to wrap PageStackNavigator in a HeroControllerScope, like this:

HeroControllerScope(
  controller: MaterialApp.createMaterialHeroController(),
  child: PageStackNavigator(
    stack: pageStack,
  )
)

Design goals

  • Integrated: work with the Flutter Navigator 2.0 API, don't try to replace it. Try to have a very Flutter-like API.
  • Usable: design around user scenarios/stories, such as the ones in the Flutter storyboard - see here for examples.
  • Opinionated: don't provide 10 options to achieve a goal, but be flexible for all scenarios.
  • Focused: just navigation, nothing else. For example, no dependency injection.

This project builds on page_router.

Name

Named after the original Routemaster:

A photo of a Routemaster bus

(photo by Chris Sampson, licensed under CC BY 2.0)

routemaster's People

Contributors

alexlindroos avatar cillianmyles avatar josephelliot-wk avatar rastislavmirek avatar roughike avatar smihica avatar thebenforce avatar thomas-frantz avatar tomgilder avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

routemaster's Issues

Question: How to keep screen state with tab navigation destinations?

First of Tom @tomgilder, thanks for this wonderful Nav2 based package, really impressive work!

As you already know I've been trying it out a bit and have a bunch of issues, probably they are more just like use cases I have not figured out how to implement with it yet. I will be posting them more as open questions for now.

This is the first one.

When using routemaster, how can we keep the state of the pages when using tab screens?

I made an example demonstrating the question. I used your mobile_app demo bundled with the package as a case example. I just added some grids to a couple of its screens (notification one and two and settings) so they become scrollable so we can easily see when the scrolling position and state is kept or lost.

As can be seen in the gif further below, it all works fine when using the bottom cupertino navigation, as that thing stores each page route in an off stage stack via its internal _TabSwitchingView. (I will have another question on this nice, desired behavior and duplicating it for none Cupertino tabs and scaffolds in another question).

However, in the tab bar page route on the Feed page, if you scroll and switch pages, you loose the scroll position when you switch tabs. When I build tab bar views normally, the scroll position of each tab view is usually retained, which is certainly a desired feature when switching left/right in tab views. So far I was unable to duplicate that behavior with routemaster and the mobile_app example. The GIF animations below demonstrates it, by first showing that it is nicely kept on the Cupertino tabs, but not on tab bar view in Feed.

routemaster-q1

You can find the modified mobile_app repo with the above grids added to it here: https://github.com/rydmike/mrnavdemo

Return values

With the classic Navigator, you can return values from pushes:

final result = await Navigator.of(context).pushNamed('route');
Navigator.of(context).pop('result');

Currently, Routemaster has no support for this.

Questions

  1. Is this something worth adding? How popular is this?
  2. Can this actually be done with Navigator 2.0 and global routing?

If this isn't added, there should be documentation on how to pass values to previous pages.

Scenario: nested but non-indexed routes

There might be a scenario where you'd want an embedded nested navigator with a stack of pages, but not using a tab bar or other indexed route.

For instance, two stack next navigators visually next to each other.

Naming of RouteData

Need to decide on the naming of RouteData. Possibly better named PathInfo?

This will primarily be used by users to get the current path: RouteData.of(context).path

Sub page broken when shares prefix with page in TabPage

I ran into this bug where if I have a TabPage /test with a child path one, then if I try to route to a sub page, not in TabPage's path property such as /test/onepage, it will render incorrectly. There is no way to go back and it renders inside of /test like a nested route rather than on top. It seems like if it matches part of a path in the "path" property of the TabPage it does this. If it doesn't contain 'one' in this example, it works as expected.

You can easily replicate this in the mobile_app example if you change /bottom-navigation-bar/sub-page to /bottom-navigation-bar/threepage.

Provide a way to only listen to top level navigator

Should have a way to only listen to top level Navigator, and not all of them

Options:

  1. Allow users to provide their own StackNavigator widget
  2. Add a navigatorObserver to RoutemasterDelegate - but that could be easily confused for observers

โ˜‚๏ธ Performance

Possible areas to look at performance, but it needs measuring:

  • _getOrCreatePageWrapper loops over current routes every time. Could be better using a map.
  • Ensure any uses of _getCurrentPages (such as from _updateCurrentConfiguration() are minimal as it has to go through entire route hierarchy

Directly going to a particular route doesn't work in release.

Hey! So I have successfully implemented route master and it works perfectly for Navigation from one page to another with URL change. You can check out at: https://knuggies.netlify.app
First Splash screen is there and then home screen.
However, if you directly go to https://knuggies.netlify.app/home it shows 404.

However, the same steps works with localhost while debugging.

Do I need to setup anything for the same?
The sample code can be checked in issue #95

Scenario: popping multiple pages

Pages A, B and C are on the navigation stack. User is on page C at path /a/b/c.

We want to pop back to page A... how? Answer: push('/a') - but that feels weird.

From a user's perspective, it feels like you want to call popTo('/a').

Needs documentation and/or better API.

Advanced routing: relative paths and wildcards

Sometimes the same page should be visible at multiple routes.

For instance a ProductPage could appear at both:

  • /search/results/product/1
  • /category/5/product/1

Currently you'd just have to provide two entries in the map. Could provide other methods such as an Alias page proxy.

Swapping routing map does not switch displayed page

Firstly, I am not sure if it is a bug or just a wrong assumption of mine.
Swapping routing maps at runtime based on the loggedIn state of my app seems to only work sometimes.

My routing maps look as follows:

final loginRoutes = RouteMap(
  routes: {
    '/login': (routeData) {
      print('about to return loginPage');
      return MaterialPage(child: LoginPage());
    },
  },
  onUnknownRoute: (route, context) {
    print('redirecting to loginPage');
    return Redirect('/login');
  },
);

final routes = RouteMap(
  routes: {
    '/': (_) => MaterialPage(child: HomePage()),
    '/test': (_) => MaterialPage(child: TestPage()),
  },
  onUnknownRoute: (route, context) => MaterialPage(child: NotFoundPage()),
);

On app start the correct routing map is picked and displayed based on the loggedIn state.
Logging in also works, i.e. the LoginPage is displayed, the loggedIn state set and the HomePage shown.
Logging out is where it gets tricky:
While on the HomePage, changing the loggedIn state causes the LoginPage to be displayed.
While on the TestPage however, changing the loggedIn state does not cause the LoginPage to be displayed. Instead the TestPage remains shown.
I have tried switching my pages around to see if the page content somehow causes it, but in the end I was able to reproduce the behaviour even with my TestPage which has basically no content at all.
This happens on any page that is not the 'home' page (i.e. '/').

Subsequent navigation does indeed lead to the LoginPage, but I'd expect the current page to be replaced immediately.
It might be relevant that the above print statements in loginRoutes do get executed.

Support regular expressions in path templates

We could support regular expressions in route maps, if there's a want for it.

For what it's worth, I don't think this is a good idea. It really complicates routing maps, and I don't think there are many good uses for it - but I'm willing to be proved wrong ๐Ÿ˜

Scenario: redirect to route after login

Come up with a demo of redirecting after a redirect:

  1. User tries to load /profile/1
  2. User isn't logged in, so gets redirected to /login
  3. User logs in - needs redirecting back to /profile/1

API question: should we use RouteData or just a string?

Routemaster current extends RouterDelegate<RouteData>, but it could potentially just extend RouterDelegate<String>.

RouteData currently just wraps a route string with nothing else. If there's nothing going to be added to this, we should just use a string.

  • Is there a reason to provide an object other than String?
  • Might we want to add extra data to the routing info later?
  • Could this play a part in state restoration?

โ˜‚๏ธ API naming

StackNavigator

  • Class name - alternatives PagesNavigator or PageStackNavigator

Guards

  • validate could be canNavigate
  • onValidationFailed would need to change to

Delegate

  • routesBuilder could be mapBuilder, builder or routeMapBuilder

RouteData

  • Class could be called PathInfo
  • path - could be route or url

Current plan for 0.8.0

  • Rename StackNavigator to PageStackNavigator
  • Deprecate guards, but rename validate to canNavigate and onValidationFailed to onNavigationFailed for anyone who really wants to use them

Pages get rebuilt when not needed with PageStackNavigator in v0.8.0

Pages get rebuilt in v0.8.0 with PageStackNavigator. This does not happen with v0.7.2 and StackNavigator. E.g. when using StatefulWidget for a page, initState is called again and everything is rebuilt from scratch, even when it should not be.

I do not get this rebuild if I downgrade to v 0.7.2 with the same project, but with v0.8.0 I get them. The only change between the builds is changing from StackNavigator to PageStackNavigator.

If I use the deprecated name/version StackNavigator with v0.8.0 the extra rebuild still happens. A downgrade to version 0.7.2 is required to avoid it and to get things working as before.

Sorry I have not looked into the issue enough yet to have minimum repeatable case. I might post an example later.

Wrapping a specific set of routes

Hey Tom

Super cool package! The dynamic route map based on state is a new idea I haven't really seen before , but I really like it

I couldn't find this in the examples, but I was wondering if there is a way to wrap a specific set of routes with other widgets.

an application of this would be to wrap all /groups routes with a GroupsBloc

Thanks~

API naming decision: Plan.routes

Most pages only appear in one place in the route tree, and this is the default constructor for plans, for example:

WidgetPlan('/search', (_) => SearchPage())

But sometimes pages can appear at multiple routes, for instance:

WidgetPlan.routes(
    ['/search/product/1', '/by-category/clothes/product/1'],
    (_) => HeroPage(),
)

WidgetPlan.routes could also be called:

  • WidgetPlan.multiple
  • WidgetPlan.many
  • WidgetPlan.various
  • WidgetPlan.several

Routemaster conflicts with url_strategy

I have created a splash screen kinda thing and in it's initState I am adding a timer and moving to next screen after 1 second it should move to next screen, however, it doesn't move to the next screen when using replace()

Chrome integration tests

We should have integration tests for Web, especially for:

  • .replace('/route') with path URL strategy - this is the most likely to have bugs
  • .replace('/route') with hash URL strategy - less likely to have issues but should still be covered

These are the only two places with web-specific code.

Scenario: route modularisation

If there's a large app with multiple teams, different teams may wish to produce their own route lists.

This needs a demo.

This should be fairly easy to do, it's just combining lists.

Store page state

Add an option to never have pages created, no matter how the user navigates around.

The widgets stay in memory and get restored to their previous state so things like scroll position and entered text remain the same.

Page titles

Possibly add support for dynamically changing the pag titles on web.

See https://stackoverflow.com/questions/60456687/how-to-dynamically-change-the-app-title-in-flutter-web/60456688#60456688

This needs careful thought on the API.

It should probably be:

  • Easily customisable: support different ways to extract titles.
  • Localisable
  • Dynamic so pages can update the title

Potential ways:

  • MaterialPage has a name property
  • Some sort of Title widget that can be placed in a page
  • Title page wrapper - Title(title: 'My Page', child: MaterialPage(child: MyPage()))

API naming decision: TabPlan

TabPlan is used to create with multiple children, but which only the one at the current index.

By far the most common use for this will be tab bars, but there are potentially other uses for this.

TabPlan(
  '/',
  (_, tabRoute) {
    return HomePage(tabRoute: tabRoute);
  },
  paths: [
    '/feed',
    '/search',
    '/notifications',
    '/settings',
  ],
),

Other options include:

  • IndexedPlan
  • SwitchingPlan
  • NestedPlan

Guard builder BuildContext

I noticed you're replacing the child property with a builder for Guard. Is there a way to allow for BuildContext to be included with the builder? I don't know anything about Navigator 2.0 so I'm sure if this is possible, but since context is included in the other two properties of Guard I figured maybe could be.

Async routes or guards

There are some use cases where being able to use async within route builders would be useful.

But this adds complication and isn't as easy as it might sound.

Will be looked into after a stable 1.0.

Customisable hero controller

Currently the hero controller is hard-coded to use the Material animation curve. Users should be able to provide their own.

Questions: Use cases and questions when trying Routemaster with a custom adaptive and responsive scaffold

Hi @tomgilder, I'm really enjoying exploring Routemaster. So impressed by it!

While trying to use Routemaster on the demo application for my still unpublished and not yet fully ready Flexfold adaptive and responsive scaffold package (info here https://rydmike.com/) and live older version of the the demo app here https://rydmike.com/demoflexfold.

I ran into some use case issues or maybe more like questions. However, in order to present them I first need to explain a bit what Flexfold is and how it works.

Flexfold info

Flexfold works a lot like the Navigation Rail in Flutter SDK, but it handles also bottom navigation bar, drawer, rail and side menu, plus a sidebar. And it has a lot of design features and behavior parameters. Also when using Flexfold on variable size canvas, the transition between the layouts for the different navigation types are animated, it just looks cooler than a sudden jump to the new responsive layout view.

The setup of navigation destinations is similar to the Navigation Rail, you just define a bunch of destinations, with labels, icons, tooltips and also path based named routes if you like. Then you give this as config data to Flexfold.
When you click on destinations in the Flexfold scaffold it just returns via a callback what the destination was. You handle the actual routing yourself, so it is totally agnostic to what kind of routing you use, as it should be.

Flexfold Destinations

With Flexfold you just define destinations very much like for the navigation rail, but it has a few more options. For the the live demo app it looks like this:

image

This will when used with the the Flexfold scaffold result in scaffold that looks like below in the main different responsive use cases:

Flexfold phone size view

flexfold-phone3

  • Destinations defined to be in the bottom bar are shown in a bottom navigation bar at phone size.
  • Bottom bar is either Material or Cupertino (experimental, now also MaterialYou). It can even be defined to be platform adaptive. The one used above is Material, but it has style parameters set that makes it mimic the nice frosted glass look of the Cupertino one, just an extra feature that can be used if so desired.
  • The bottom bar can hide when scrolling, does not have to, but often useful.
  • Destinations that were not defined to be in the bottom bar, show up in the drawer. The drawer can also accept a few custom top, bottom and footer widgets if so desired.
  • In the demo when you navigate, the transition is different when using bottom nav, than compared to tablet rail mode, or desktop size mode. This is just an implementation in the the demo app, it uses return info from Flexfold destination callback to determine what responsive state was navigated from and the used navigator uses a different transition based on that info. The demo app uses slight slide action in bottom nav bar mode. I need to pass this info somehow to routemaster and setup custom transitions there too based on where the navigation was started from.
  • The destinations relegated to the drawer in the phone view are pushed on top when navigated to from the drawer as a new screen and have a back button. It does not have to do so, the home option in the demo actually does not do so. If you use a destination that does not push the screen on top, but is not a member of the bottom bar destinations, then if you open the drawer, the bottom destinations will be present in the drawer as well. If you open the drawer when a bottom destination is selected, the bottom destinations are not included in the drawer. It is also possible to define a behavior where bottom destinations are always present in the drawer as well, but then you have two ways to navigate to same destination, some might find that confusing UX, so it is not the default, even if I like it.
  • The live demo keeps the scroll state of the tab bar page destination when you are on that page, but it is not kept for the bottom ones in the demo, but I will add that as well in the next update to the live demo.

Flexfold tab size view

flexfold-tablet1

  • In the tablet size view, all the destinations are shown in a navigation rail style UI (It is not using the SDK rail though).
  • The labels are used as tooltips here. In the bottom navbar case when labels are shown, they are not duplicated as tooltips.
  • You can also add tooltips that are different from the labels, if so they are always shown. Unless Flexfold flag to totally disable tooltips is set.
  • If the Flexfold settings has been configured to allow it, the user can toggle between rail/bottom bar view in tablet size (shown in the gif). You can also set it up so that using a drawer is toggleable as well, this not shown in the above GIF. The use case is of course that if you on a larger canvas want to really maximize its size for the shown content, the user can do so.
  • The navbar can show all destinations that we defined in the setup above. The destinations that in bottom nav bar mode were in the drawer and pushed on top, are now instead always shown in the body content area, not pushed on top as new screen with back button.
  • The transition is now different than when we navigated from bottom bar, it uses a slight zoom fade through in this demo. This is again just implemented in the demo app's current navigator based on where Flexfold told us it we navigated from.
  • In the demo app we also want the transition to ONLY affect the content area. On a large canvas it looks distracting if the entire page uses a transition, but if only the content transitions, it is quite OK. So the rail, and app bar remain fixed, only the content is transitioned when navigating. The page we navigate to is actually just the body part, and the navigator is nested in Flexfold scaffold's body part. This is actually also the case for the bottom nav bar mode in the phone view, if you look closely you can notice it there too, only the content transitions there as well, but the bottom navbar an app bar stay in place, as I want it.

Flexfold desktop size view with side bar

flexfold-desktop1

  • This view is pretty equivalent to the tablet view with the following additions.
  • Destinations can, but does not have to, have a sidebar.
  • Destinations are shown as a side menu with labels.
  • If allowed in Flexfold settings, the user can still toggle the menu from menu size, to rail, and even to drawer. And also close the side bar and hide it as an end-drawer.
  • In this case when navigating in desktop mode, there is no transition at all. This is just a design choice in the demo app, I might ad some minor subtle very quick fade through later to the demo, just to show you can make decent transitions for larger surfaces too. Again the transition is a part of the demo app and its current routing, where the router gets as info what Flexfold mode was used to select the new destination.

Flexfold destination info.

In the demo app you can see the type of info it returns based on what mode was used to go to a new destination:
image

You get an overall menu index, bottom index and the named path route you defined for the destinations. Plus an enum indicating what was used: bottom, drawer, rail, menu, plus direction reverse: false/true (moving to lower or higher index), both these are intended to be used as info for a way to vary the transition based on where user clicked to go to the destination. The reverse info can be used to have a slight left/right type of slide on bottom bar or up/down on rail/menu, if so desired.

image

What you do and how you navigate based on the 'onDestination' info is up to whatever used navigation implementation.

Challenges with Routemaster and Flexfold demo app

I had on purpose left out all fancy Web based URL navigation from the Flexfold web demo. Simply because I had no simple and quick way of doing it. I now wanted to try and see if I can solve it it with your wonderful Routemaster package.

Mostly I think I can, and if I can't it is perhaps just because I'm not yet 100% familiar with all its possibilities. It certainly has much more features than I need when it comes to web routing, at least in this simple demo. I might even add some more features to the demo later to show off some more capabilities that your router solves beautifully.

Here are some of the challenges and question I have stumbled on so far:

  • General state storing navigator - partially solved - but web URL entry does not navigate.
  • A Tab or Cupertino Page content as a full page pushed on top of root "/" route.
  • Transition selection.

A) General State Storing Navigator - partially solved - but web URL entry does not navigate

In the demo (and another more real use case too) I want to store and keep the state of all the destinations that are available as direct "top" destinations in the Flexfold scaffold.

The CupertinoTabScaffold, CupertinoTabBar, CupertinoTabPage and the StackNavigator setup, like you do in the mobile_app demo is a great way of doing this. But of course we cannot use a CupertinoTabScaffold in this use case, and we also have no CupertinoTabBar, (even if one might be baked into the Flexfold scaffold, if that is used as bottom UI widget), when using the Flexfold scaffold.

For the Flexfold demo app, using the above shown destinations, I made this simple test setup to try routemaster:

image

and for buildRouteMap this worked well enough for the simple demo app:

image

As an experiment I used CupertinoTabPage as my root route. The child LayoutShellWrapper is a Widget the has the Flexfold scaffold in it, so no CupertinoTabScaffold. For the body it just uses a variant of what the CupertinoTabScaffold uses internally to keep the pages in memory off stage when so needed. For this I just copied the implementation detail, namely the _TabSwitchingView and made a PageSwitchingView version of it. It is identical to it, only public and some props renamed a bit.

With it, in the Flexfold scaffold's build I can still do:

image

In its onDestination we can just set:

image

The Flexfold has a body property like a Material scaffold, so we can then do this in its body:

image

This all actually works beautifully and state is kept when navigating between all "top" destinations in the Flexfold scaffold and they all have their own stack as well.

I noticed that in the onDestination I can instead of setting the controller index, also navigate to the page with the returned
path by doing a:

Routemaster.of(context).push(destination.route);

This is nice because it means it will easily even support my "modules" need for Flexfold destinations.

This is just a simple idea where destinations could also have module ID and you can swap out shown destinations in Flexfold by selecting the module. The modules can of course share some routes, like home, help, about etc... if so desired.

This just means that this setup could be used to always navigate to the right page based on the returned path from the destination and just ignore the indexes. The indexes are just used to hold and store all the pages in the for the keeping state when navigating to different destinations even between modules. This consumes more ram for sure, but for my particular use case it is what is needed on desktop and web. No idea how well it will perform yet. If it becomes to memory hungry I can also go back to the idea that only navigation within a module keeps state. The app implementation would just keep which index was last used in each module, so you are at least back on same page where you where when switched to another module. But if it performs OK while keeping state of all pages, that is even nicer UX wise.

This all seemed to work very well, but...

Except, this setup seem like it has no hooks into handling the URL entry from the browser (or back button). The browser URL updates when navigating from the UI fine, but because I'm not using a CupertinoTabScaffold I am of course not getting all the nice browser URL entry based navigation you baked into routemaster when using CupertinoTabScaffold. I have not yet figured out how I can get it to also update it based on browser URL entry when using routemaster and my PageSwitchingView.

It now keeps the state of top destinations, like I want it to, and also the tabs. The tab sub views on the tabs destination is just a normal routemaster tab page view, the sub pages for the tabs page are not part of the Flexfold destination, only the tab page is, so for that those pages the URL entry based update of the tabs, and browser back, even works OK.

Here is a slowish debug build (with low GIF framerate o make it small so it fits on GH) showing the Flexfold demo using routemaster with the URL updating OK when navigating, with state of all main destinations kept, and URL based navigation entry even working on the tabs page too.

flexfold-routemaster4

However on the main destinations the URL entry does not work, since there is no built-in support for it. I wonder if there is some hook I could tie into in order to add it? Or if this is a case where support for something like the PageSwitchingView() in routemaster is needed?

Since I am using my custom PageSwitchingView() to store the page states like the CupertinoTabScaffold does internally. I don't actually need the root to be a CupertinoTabPage I can just as well use an IndexedPage, but it did not help with the URL parsing. The mobile_app examples uses the Cupertino thing again for the IndexedPage, so there was no guidance there. Perhaps there is something I'm missing in the setup, hmm...

B) A Tab or Cupertino Page content as a full page pushed on top of root.

The other case I'm wondering about. How I can when the onDestination returned destination has destination.useModal set to true, how can I then instead of using the PageSwitchingView view in the body, instead push it as new screen on top of the entire LayoutShellWrapper while using (showing) the same route path as it has in the wrapper view? A need to wrap the content in a Scaffold with an AppBar might be there, although in the live demo in its nested nav1 I could actually even avoid that. With nav 1, I just used another navigator, well the root one and not the one nested into the body of Flexfold, and pushed it on top of the stack with that covering the Flexfold wrapper and its "body" nested navigator.

EDIT (28.5.2021): Tried a bunch of things, but nothing really produces the correct result.

I feel to do this I must have access to a lower root navigator, than the first IndexedPage tied to root "/" route with its PageStackNavigator to be able to push a page on top of it. Also when I trigger that one, I need to prevent the navigator tied to the root "/" from navigating, ie showing the same page inside the scaffold that it normally wants to do in another layout scenario, instead in this scenario I want to push the page on top of everything and the page stack below must remain where it is, so when the page pushed on top of it is popped, it remains where it was. Hmm... I will experiment more I guess. Feel a bit stuck though.

C) Transition selection

Based on the examples and docs, it does seems like I should be able to grab the Flexfold destination.source (drawer, bottom, rail, menu) and pass it along to a page transition builder, so that when navigation happens I can switch there for the different desired transition styles, just like I'm doing in the live version of demo app with navigator 1 already.

EDIT (28.5.2021): I tried two different setups with the config described in A) and B) above, but I cannot get any page transitions with it regardless what I try.

  • Tried a custom Page extending Page that gets a parameter with the info of what transition it should do, and has different transitions based on it. But, I get no transitions at all.
  • Tried another approach, using a PageTransitionSwitcher from the Flutter Animations package, this baked into the PageSwitchingView, that has PageTranstionSwitcher for the StackNavigator for the root "/" route, but nope no transitions with that either. Page switches worked though, just no transitions with that setup either.

Maybe it is custom PageSwitchingView that kills them, it contains some no off stages transition thing. If that is it, I should see the same issue with CupertinoTabScaffold that uses it as well, I will check that out.


Sorry this was a super long questions posts, but I like to put things in context so the use case and scenarios can be properly understood. Not sure I managed to do so, I tend to be so darn verbose, hehe ๐Ÿ˜„

So far I think routemaster rocks ๐Ÿ’ช๐Ÿป ๐Ÿš€ , and if I can get the above cases to work, I have no reason to build anything from scratch with Nav2 for any use case.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.