我正在尝试为以下问题找出良好的架构解决方案:我有以下第一级路由,也可以称为布局:
/onboarding/* -> Shows onboarding layout
/dashboard/* -> Shows dashboard layout
/overlay/* -> shows slide up overlay layout
/modal/* -> shows modal layout
用户根据他/她的身份验证状态,操作等路由到这些中的每一个。我正确地完成了这个阶段。
当我想使用二级路线(可以称为页面)时出现问题,例如
/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.
如何以可以有效地路由到显示的布局和页面的方式来最好地定义/组织这些?请注意,每当我在一个布局中布置页面时,布局都不会改变,但我想根据路径为内部(页面)内部的内容(页面)设置动画。
到目前为止,我实现了以下
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)));
}
这将路由到登机和仪表板布局(现在只是简单的容器包装文本)。我也相信我可以使用PageRouteBuilder
后者来设置路线之间的过渡动画?现在我需要弄清楚如何在登机和仪表板上安装嵌套的辅助路由器。
下面是我想要实现的一些视觉表示,我需要能够成功地路由蓝色和红色位。在此示例中,只要我们在/dashboard
下蓝色位(布局)没有变化,但是当我们从说/dashboard/home
导航到/dashboard/stats
时,红色位(页面)应该淡出淡出新内容。如果我们从/dashboard/home
导航到/onboarding/home
,红色位(布局)应该逐渐消失,以及当前活动的页面并显示新的入门布局,故事仍在继续。
编辑我通过下面概述的方法取得了一些进展,基本上我将确定我的runApp
内的布局,并将声明新的WidgetsApp
并在每个内部路由布局。它似乎有效,但有一个问题,当我点击&#34; SignUp&#34;我被重定向到正确的页面,但我也可以看到它下面的旧页面。
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;
});
}
答案 0 :(得分:8)
虽然技术上可以嵌套“Navigator”,但这里不推荐(因为它打破了 Hero 动画)
您可以使用onGenerateRoute
构建嵌套的“路由”,如果是路径'/ dashboard / profile',则构建树WidgetApp > Dashboard > Profile
。我认为这是你想要实现的目标。
结合更高阶的功能,您可以拥有为您创建onGenerateRoute
的内容。
提供代码流的线索:NestedRoute
忽略了布局的确切构建,将其放在builder
方法中(例如 {{1} })。在调用builder: (child) => new Dashboard(child: child),
方法时,我们会为此页面的实例生成buildRoute
,但让PageRouteBuilder
管理_build
的创建。在Widgets
中,我们要么原样使用_build
,要么通过调用请求的子路由来调用自己的builder
来让它膨胀子路由。完成后,我们将使用构建的子路由作为构建器的参数。长话短说,你递归地进入更远的路径级别来构建路径的最后一级,然后让它从递归中升起并使用结果作为外层的参数等等。
_build
为您完成脏工作并解析BuildNestedRoutes
列表以构建必要的NestedRoutes
。
所以,从下面的例子
示例:
RouteSettings
在这里,您只需定义嵌套路由(名称+关联组件)。
@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'),
)
],
),
],
),
);
}
class + NestedRoute
方法以这种方式定义:
buildNestedRoutes
这样,您的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);
};
}
和Foo
组件就不会与您的路由系统紧密耦合;但仍然有嵌套路线。
然后在整个地方派遣您的路线更具可读性。你可以轻松添加一个新的。
答案 1 :(得分:4)
您可以嵌套使用标准Navigator,而无需任何其他技巧。
您所需要的就是分配一个global key并指定必要的参数。当然,您需要关心android后退按钮behaviour。
您唯一需要知道的是此导航器的上下文将不是全局的。使用它会导致一些特定的观点。
下面的示例稍微复杂一些,但它使您可以看到如何为导航器小部件设置从外部和内部的嵌套路线。例如,我们在根页面中调用setState
,以通过initRoute
中的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'),
),
],
),
),
);
}
}
您可以在next article中找到一些其他信息。
很遗憾,you cannot navigate to same root widget without navigation stack是您唯一的孩子时。所以,为了避免 您需要创建自定义导航方法的根窗口小部件导航(根窗口小部件重复) 基于InheritedWidget的示例。您将在其中检查新的根目录 路由,如果未更改为仅调用子嵌套导航器。
因此,您需要将路线分为两个部分:“ / onboarding”用于 根导航器和嵌套导航器的“ / plan”并处理此数据
答案 2 :(得分:3)
您尝试构建的模式,即使是合理的,似乎也无法用Flutter开箱即用。
编辑:您想要实现的行为需要使用onGenerateRoute,但尚未(1月18日)正确记录(doc)。请参阅@Darky的回答以举个例子。他建议NestedRouteBuilder
和NestedRoute
实施,填补空白。
使用MaterialApp中的普通导航器,路由和页面导航(根据doc)有两个主要特征,即否定您想要实现的目标(至少直接)。 一方面,Navigator
表现为堆栈,因此推送和弹出路由一个在下一个上面,依此类推,在另一个上路径是全屏或模态 - 这意味着它们会部分占据屏幕,但它们禁止与下方的小部件进行交互。更明确的是,您的范例似乎需要与堆栈中不同级别的页面同时进行交互 - 这不能通过这种方式完成。
此外,感觉路径范例不仅仅是层次结构 - 一般框架→特定子页面 - 而是在第一个实例中在导航器中表示堆栈。我自己被欺骗了,但很清楚阅读this:
String initialRoute
final
要显示的第一条路线的名称。
默认情况下,这是指dart:ui.Window.defaultRouteName。
如果此字符串包含任何/字符,则拆分字符串 从字符串开头到的那些字符和子串 反过来,每个这样的角色都被用作推进的路线。
例如,如果使用route / stocks / HOOLI作为initialRoute, 然后导航器会在启动时推送以下路由:/, /股票,/股票/ HOOLI。这允许深度链接,同时允许 用于维护可预测路线历史的应用程序。
一个可能的解决方法,如下所示,是利用路径名实例化子窗口小部件,保持状态变量知道要显示的内容:
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,
),
],
),
);
}
}
然而,这在较低级别没有堆栈内存。如果你想处理子路径小部件的顺序 - 你可以将子路由容器包装在WillPopScope
中,定义当用户按下back
按钮时应该做什么,并存储序列堆栈中的子路由。但是,我不想暗示这样的事情。
我的最终建议是实现普通路由 - 没有“级别” - 管理自定义转换以隐藏“外部”布局的更改并通过页面传递数据或保留适当的类,为您提供应用状态。
PS:同时检查Hero动画,它们可以为您提供视图之间的连续性。