有颤动的筑巢路线

问题描述 投票:18回答:3

我试图找出以下问题的良好架构解决方案:我有以下第一级路线,也可以称为布局:

/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,红色位(布局)应该逐渐消失,以及当前活动页面并显示新的入门布局,故事仍在继续。

enter image description here

编辑我用下​​面概述的方法取得了一些进展,基本上我将确定我的runApp内的布局,并将在每个布局中声明新的WidgetsApp和路由。它似乎工作,但有一个问题,当我点击“SignUp”我被重定向到正确的页面,但我也可以看到它下面的旧页面。

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;
      });
}
routing dart flutter
3个回答
9
投票

虽然在技术上可以嵌套“Navigator”,但这里不推荐(因为它打破了Hero动画)

你可以使用onGenerateRoute来构建嵌套的“路由”,在路由'/ dashboard / profile'的情况下,构建一个树WidgetApp > Dashboard > Profile。我认为这是你想要实现的目标。

结合更高阶函数,您可以拥有为您创建onGenerateRoute的东西。

为了提供代码流的线索:NestedRoute忽略了布局的确切构建,让它在builder方法中(例如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'),
            )
          ],
        ),
      ],
    ),
  );
}

在这里,您只需定义嵌套路由(名称+关联组件)。而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);
  };
}

这样,您的FooBar组件将不会与您的路由系统紧密耦合;但仍然有嵌套路线。然后在整个地方派遣您的路线更具可读性。你可以轻松添加一个新的。


4
投票

你可以使用标准的Navigator作为嵌套,没有任何额外的技巧。

enter image description here

所有你需要的,是分配一个global key并指定必要的参数。当然,你需要关心android后退按钮behaviour

您唯一需要知道的是该导航器的上下文不是全局的。它将导致一些特定的点与它一起工作。

以下示例稍微复杂一些,但它允许您查看如何为导航器窗口小部件设置外部和内部的嵌套路径。在示例中,我们在qzxswpoi的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,当你改变只有孩子。因此,为了避免根窗口小部件导航(根窗口小部件复制),您需要创建自定义导航方法,例如基于InheritedWidget。在其中,您将检查新的根路由,如果未更改为仅调用子(嵌套)导航器。

因此,您需要将路由分为两部分:“/ onboarding”用于根导航器,“/ plan”用于嵌套导航器,并分别处理此数据。


3
投票

您尝试构建的模式,即使是合理的,似乎也无法用Flutter开箱即用。

编辑:您想要实现的行为需要使用onGenerateRoute,但尚未(1月18日)正确记录(you cannot navigate to same root widget without navigation stack)。请参阅@Darky的回答以举个例子。他提出了docNestedRouteBuilder实现,填补了空白。

使用MaterialApp中的普通导航器,路由和页面导航(根据NestedRoute)有两个主要特征,否定您想要实现的目标(至少直接)。一方面,doc表现为一个堆栈,因此推送和弹出路由一个在下一个上面,依此类推,在其他路由上是全屏或模态 - 意味着它们部分占据屏幕,但它们抑制了与下面的小部件。更明确的是,您的范例似乎需要与堆栈中不同级别的页面同时进行交互 - 这不能以这种方式完成。

此外,感觉路径范例不仅是层次结构 - 一般框架→特定子页面 - 而且首先是导航器中堆栈的表示。我自己被欺骗了,但是很清楚阅读Navigator

this

String initialRoute

要显示的第一条路线的名称。

默认情况下,这遵循dart:ui.Window.defaultRouteName。

如果此字符串包含任何/字符,则字符串将拆分为这些字符,并且从字符串开头到每个此类字符的子字符串将被用作推送路由。

例如,如果route / stocks / HOOLI用作initialRoute,则导航器将在启动时推送以下路由:/,/ stocks,/ stocks / HOOLI。这样可以实现深层链接,同时允许应用程序维护可预测的路径历史记录。

如下所示,可能的解决方法是利用路径名来实例化子窗口小部件,保持状态变量以了解要显示的内容:

final

然而,这在较低级别没有堆栈存储器。如果你想处理子路径窗口小部件的序列 - 你可以将子路径容器包装在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按钮时应该做什么,并将子路径的序列存储在堆栈中。但是我不想暗示这样的事情。

我的最终建议是实现普通路由 - 没有“级别” - ,管理自定义转换以隐藏“外部”布局的更改并通过页面传递数据或保持适当的类提供应用程序状态。

PS:同时检查back动画,它们可以为您提供视图之间的连续性。

© www.soinside.com 2019 - 2024. All rights reserved.