Flutter Attachment Routes

I am trying to find a good architectural solution for the following problem: I have the following first-level routes , which can also be called layouts:

/onboarding/* -> Shows onboarding layout /dashboard/* -> Shows dashboard layout /overlay/* -> shows slide up overlay layout /modal/* -> shows modal layout 

The user is redirected to each of them depending on his condition, actions, etc. I correctly understood this stage.

Problems arise when I want to use Secondary level routes, which can be called pages, for example

 /onboarding/signin -> Shows onboarding layout, that displays signin route /onboarding/plan -> Shows onboarding layout, that displays plan options /modal/plan-info -> Shows modal layout, over previous page (/onboarding/plan) and displays plan-information page. 

How can I best define / organize them so that I can efficiently route the layouts and pages that they display? Please note that whenever I route pages within the same layout, the layout does not change, but I want to animate the contents (pages) that change inside it based on the route.

So far I have achieved the following results:

 import "package:flutter/widgets.dart"; import "package:skimitar/layouts/Onboarding.dart"; import "package:skimitar/layouts/Dashboard.dart"; Route generate(RouteSettings settings) { Route page; switch (settings.name) { case "/onboarding": page = new PageRouteBuilder(pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return new Onboarding(); }); break; case "/dashboard": page = new PageRouteBuilder(pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return new Dashboard(); }); break; } return page; } /* Main */ void main() { runApp(new WidgetsApp( onGenerateRoute: generate, color: const Color(0xFFFFFFFFF))); } 

This is the route to the layouts on board and on the control panel (right now the text of the container is simply transferred). I also believe that I can use the last PageRouteBuilder to animate transitions between routes? Now I need to figure out how to do something like a nested secondary router inside on board and on the control panel.

Below is a somewhat visual representation of what I want to achieve, I need to be able to successfully route the blue and red bits. In this example, while we are under /dashboard blue bit (layout) does not change, but as we move from say /dashboard/home to /dashboard/stats red bit (page) should disappear and disappear with new content, If we move away from /dashboard/home , to say /onboarding/home , the red bit (layout) should disappear along with its active active page and show a new layout for navigation, and the story continues.

enter image description here

EDIT I did a little work with the approach described below, in fact, I will define a layout inside my runApp and declare a new WidgetsApp and lay inside each of the layouts. This seems to work, but there is a problem. When I click "SignUp", they redirect me to the correct page, but I can also see the old page below.

main.dart

 import "package:flutter/widgets.dart"; import "package:myProject/containers/layouts/Onboarding.dart"; /* Main */ void main() { runApp(new Onboarding()); } 

Onboarding.dart

 import "package:flutter/widgets.dart"; import "package:myProject/containers/pages/SignIn.dart"; import "package:myProject/containers/pages/SignUp.dart"; import "package:myProject/services/helpers.dart"; /* Onboarding router */ Route onboardingRouter(RouteSettings settings) { Route page; switch (settings.name) { case "/": page = buildOnboardingRoute(new SignIn()); break; case "/sign-up": page = buildOnboardingRoute(new SignUp()); break; default: page = buildOnboardingRoute(new SignIn()); } return page; } class Onboarding extends StatelessWidget { @override Widget build(BuildContext context) { return new Container( decoration: new BoxDecoration( color: const Color(0xFF000000), image: new DecorationImage( image: new AssetImage("assets/images/background-fire.jpg"), fit: BoxFit.cover)), child: new WidgetsApp( onGenerateRoute: onboardingRouter, color: const Color(0xFF000000)), ); } } 

SignUp.dart

 import "package:flutter/widgets.dart"; class SignUp extends StatelessWidget { @override Widget build(BuildContext context) { return new Center( child: new Text("Sign Up", style: new TextStyle(color: const Color(0xFFFFFFFF)))); } } 

helpers.dart

 import "package:flutter/widgets.dart"; Route buildOnboardingRoute(Widget page) { return new PageRouteBuilder( opaque: true, pageBuilder: (BuildContext context, _, __) { return page; }); } 
+15
dart routing flutter
source share
3 answers

Although it’s technically possible to attach the “Navigator”, this is not recommended here (since it violates the animation of the characters)

You can use onGenerateRoute to create nested "routes", in the case of the "/ dashboard / profile" route, build Tree WidgetApp > Dashboard > Profile . I believe that you are trying to achieve.

Combined with a higher order function, you may have something that creates onGenerateRoute for you.

To get an idea of ​​the code stream: NestedRoute ignores the exact assembly of the layout, skipping it in the builder method (for example, builder: (child) => new Dashboard(child: child), ). When calling the buildRoute method buildRoute we are PageRouteBuilder for the instance of this page itself, but letting _build control the creation of Widgets . In _build we either use builder as is - or let it inflate the subordinate route by calling the requested subordinate route, invoking its own _build . After that, we will use the built-in subordinate route as an argument to our collector. In short, you recursively plunge into further levels of the path to build the last level of the route, then let it rise from the recursion and use the result as an argument for the outer level and so on.

BuildNestedRoutes does the dirty work for you and parses the NestedRoutes lists to create the necessary RouteSettings .

So from the example below

Example:

 @override Widget build(BuildContext context) { return new MaterialApp( initialRoute: '/foo/bar', home: const FooBar(), onGenerateRoute: buildNestedRoutes( [ new NestedRoute( name: 'foo', builder: (child) => new Center(child: child), subRoutes: [ new NestedRoute( name: 'bar', builder: (_) => const Text('bar'), ), new NestedRoute( name: 'baz', builder: (_) => const Text('baz'), ) ], ), ], ), ); } 

Here you simply defined your nested routes (name + related component). And the NestedRoute class + NestedRoute method buildNestedRoutes defined as follows:

 typedef Widget NestedRouteBuilder(Widget child); @immutable class NestedRoute { final String name; final List<NestedRoute> subRoutes; final NestedRouteBuilder builder; const NestedRoute({@required this.name, this.subRoutes, @required this.builder}); Route buildRoute(List<String> paths, int index) { return new PageRouteBuilder<dynamic>( pageBuilder: (_, __, ___) => _build(paths, index), ); } Widget _build(List<String> paths, int index) { if (index > paths.length) { return builder(null); } final route = subRoutes?.firstWhere((route) => route.name == paths[index], orElse: () => null); return builder(route?._build(paths, index + 1)); } } RouteFactory buildNestedRoutes(List<NestedRoute> routes) { return (RouteSettings settings) { final paths = settings.name.split('/'); if (paths.length <= 1) { return null; } final rootRoute = routes.firstWhere((route) => route.name == paths[1]); return rootRoute.buildRoute(paths, 2); }; } 

Thus, your Foo and Bar components will not be closely related to your routing system; but there are still nested routes. This is more readable than sending your routes everywhere. And you will easily add a new one.

+7
source share

The template you are trying to build, even if it’s reasonable, seems like it cannot be presented out of the box with Flutter.

EDIT . The behavior you want to achieve requires the use of onGenerateRoute, but not yet written (Jan'18) ( doc ). See @Darky's answer for an example. It offers implementations of NestedRouteBuilder and NestedRoute , filling the gap.

Using a simple navigator from MaterialApp, navigating routes and pages (according to the doc ) are two main characteristics that deny what you want to achieve (at least directly). On the one hand, Navigator behaves like a stack, thereby pushing and popping out routes one on top of the next, etc., On other routes, either full-screen or modal - this means that they partially occupy the screen, but they prohibit interaction with widgets below . More explicitly, your paradigm seems to require simultaneous interaction with pages at different levels on the stack - this cannot be done this way.

In addition, it seems that the path paradigm is not only a hierarchy - a common frame → a specific subpage, but in the first case, the representation of the stack in the navigator . I deceived myself, but it becomes clear that we are reading this :

String initialRoute

final

The name of the first route displayed.

By default, this drops the dart: ui.Window.defaultRouteName.

If this line contains any / characters, then the line is divided into those characters and substrings from the beginning of the line to each of them, in turn, is used as routes for pressing.

For example, if the route / stock / HOOLI was used as the initial Route, then Navigator will start the following routes at startup: /, / stock, stock / HOLI. This provides deep linking, allowing the app to maintain predictable route history.

A possible workaround, as it follows, is to use the path to instantiate the child widgets, keeping the state variable in order to know what to show:

 import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: new ActionPage(title: 'Flutter Demo Home Page'), routes: <String, WidgetBuilder>{ '/action/plus': (BuildContext context) => new ActionPage(sub: 'plus'), '/action/minus': (BuildContext context) => new ActionPage(sub: 'minus'), }, ); } } class ActionPage extends StatefulWidget { ActionPage({Key key, this.title, this.sub = 'plus'}) : super(key: key); final String title, sub; int counter; final Map<String, dynamic> subroutes = { 'plus': (BuildContext context, int count, dynamic setCount) => new PlusSubPage(count, setCount), 'minus': (BuildContext context, int count, dynamic setCount) => new MinusSubPage(count, setCount), }; @override ActionPageState createState() => new ActionPageState(); } class ActionPageState extends State<ActionPage> { int _main_counter = 0; String subPageState; @override void initState() { super.initState(); subPageState = widget.sub; } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('Testing subpages'), actions: <Widget>[ new FlatButton( child: new Text('+1'), onPressed: () { if (subPageState != 'plus') { setState(() => subPageState = 'plus'); setState(() => null); } }), new FlatButton( child: new Text('-1'), onPressed: () { if (subPageState != 'minus') { setState(() => subPageState = 'minus'); setState(() => null); } }), ], ), body: widget.subroutes[subPageState](context, _main_counter, (count) { _main_counter = count; })); } } class PlusSubPage extends StatefulWidget { PlusSubPage(this.counter, this.setCount); final setCount; final int counter; @override _PlusSubPageState createState() => new _PlusSubPageState(); } class _PlusSubPageState extends State<PlusSubPage> { int _counter = 0; @override void initState() { super.initState(); _counter = widget.counter; } void _incrementCounter() { setState(() { _counter++; widget.setCount(_counter); }); } @override Widget build(BuildContext context) { return new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new IconButton( icon: const Icon(Icons.add), onPressed: _incrementCounter, ), new Text( 'You have pushed the button this many times:', ), new Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ); } } class MinusSubPage extends StatefulWidget { MinusSubPage(this.counter, this.setCount); final setCount; final int counter; @override _MinusSubPageState createState() => new _MinusSubPageState(); } class _MinusSubPageState extends State<MinusSubPage> { int _counter = 0; @override void initState() { super.initState(); _counter = widget.counter; } void _decrementCounter() { setState(() { _counter--; widget.setCount(_counter); }); } @override Widget build(BuildContext context) { return new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new IconButton( icon: const Icon(Icons.remove), onPressed: _decrementCounter, ), new Text( 'You have pushed the button this many times:', ), new Text( '$_counter', style: Theme.of(context).textTheme.display1, ), ], ), ); } } 

This, however, does not have stack memory at a lower level. If you want to process the sequence of the widget routines, you can wrap the container routines in WillPopScope , indicating there what it should do when the user presses the back button and saves the sequence of routines on the stack. However, I do not want to offer such a thing.

My last suggestion is to implement simple routes - without "levels" - to manage custom transitions to hide changes to the "external" layout and transfer data through pages or save in the appropriate class, giving you the state of the application.

PS: also check out Hero animations, they can provide you with the continuity you are looking for between performances.

+3
source share

You can use the standard navigator as nested, without any additional tricks.

enter image description here

All you need to do is assign a global key and specify the necessary parameters. And, of course, you need to take care of the behavior of the back button on Android.

The only thing you need to know is that the context of this navigator will not be global. This will lead to certain points in working with him.

The following example is a bit more complicated, but it allows you to see how you can set nested routes inside and out for a navigator widget. In the example, we call setState on the root page to set a new route using initRoute from the NestedNavigator .

  import 'package:flutter/material.dart'; void main() => runApp(App()); class App extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Nested Routing Demo', home: HomePage(), ); } } class HomePage extends StatefulWidget { @override _HomeState createState() => _HomeState(); } class _HomeState extends State<HomePage> { final GlobalKey<NavigatorState> navigationKey = GlobalKey<NavigatorState>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Root App Bar'), ), body: Column( children: <Widget>[ Container( height: 72, color: Colors.cyanAccent, padding: EdgeInsets.all(18), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Text('Change Inner Route: '), RaisedButton( onPressed: () { while (navigationKey.currentState.canPop()) navigationKey.currentState.pop(); }, child: Text('to Root'), ), ], ), ), Expanded( child: NestedNavigator( navigationKey: navigationKey, initialRoute: '/', routes: { // default rout as '/' is necessary! '/': (context) => PageOne(), '/two': (context) => PageTwo(), '/three': (context) => PageThree(), }, ), ), ], ), ); } } class NestedNavigator extends StatelessWidget { final GlobalKey<NavigatorState> navigationKey; final String initialRoute; final Map<String, WidgetBuilder> routes; NestedNavigator({ @required this.navigationKey, @required this.initialRoute, @required this.routes, }); @override Widget build(BuildContext context) { return WillPopScope( child: Navigator( key: navigationKey, initialRoute: initialRoute, onGenerateRoute: (RouteSettings routeSettings) { WidgetBuilder builder = routes[routeSettings.name]; if (routeSettings.isInitialRoute) { return PageRouteBuilder( pageBuilder: (context, __, ___) => builder(context), settings: routeSettings, ); } else { return MaterialPageRoute( builder: builder, settings: routeSettings, ); } }, ), onWillPop: () { if(navigationKey.currentState.canPop()) { navigationKey.currentState.pop(); return Future<bool>.value(false); } return Future<bool>.value(true); }, ); } } class PageOne extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Page One'), RaisedButton( onPressed: () { Navigator.of(context).pushNamed('/two'); }, child: Text('to Page Two'), ), ], ), ), ); } } class PageTwo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Page Two'), RaisedButton( onPressed: () { Navigator.of(context).pushNamed('/three'); }, child: Text('go to next'), ), RaisedButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('go to back'), ), ], ), ), ); } } class PageThree extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Page Three'), RaisedButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('go to back'), ), ], ), ), ); } } 

You can find more information in the next article .

Unfortunately, you cannot go to the same root widget without a navigation stack when you change only the child widget . Therefore, to avoid navigating the root widget (duplicating the root widget ), you need to create your own navigation method, for example, based on InheritedWidget. In which you will check the new root route and, if it is not changed, call only the child (nested) navigator.

Therefore, you need to divide the route into two parts: "/ onboarding" for the root navigator and "/ plan" for the nested navigator and process this data separately.

0
source share

All Articles