【问题标题】:Nesting routes with flutter用颤振嵌套路由
【发布时间】:2018-06-14 08:24:01
【问题描述】:

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

/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 后者来动画路线之间的过渡?现在我需要弄清楚如何在登机和仪表板上安装类似 nested secondary router 的东西。

下面是我想要实现的视觉表示,我需要能够成功地路由蓝色和红色位。在这个例子中,只要我们在/dashboard 下,蓝色位(布局)不会改变,但是当我们从/dashboard/home 导航到/dashboard/stats 时,红色位(页面)应该淡出并随着新内容淡入.如果我们从/dashboard/home 导航到/onboarding/home,红色位(布局)应该会随着它当前活动的页面一起消失,并显示新的入职布局,然后故事继续。

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

ma​​in.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;
      });
}

【问题讨论】:

  • 您想通过嵌套路由器完全实现什么?你将如何使用它?有多种方法可以获得相同的布局没有嵌套路由器。所以很难给出一个能满足你要求的答案
  • @Darky 我的应用有几个布局,每个布局都可以根据路线显示内容。我不需要这个内容是全屏的,只需要在布局中占据一定的空间。我认为通过上层的路线决定显示哪种布局也很好。
  • 抖动可能并不常见,我从 react-native 中获取了这个概念,在那里非常容易做到,但我在这里很难弄清楚。就像,假设您通过路由打开一个模态,并且这个模态内部有链接,以便在其中完成更多嵌套路由。
  • @Ilja 你接受了什么解决方案?我现在和你完全一样,一直在努力解决如何完成这件事。一点帮助?
  • @Alagh 这是一个游戏应用程序,所以最后我放弃了颤振以支持 webgl,恐怕从来没有真正达到我解决这个问题的地步。但是接受的答案有一些很好的指导,希望它有所帮助

标签: routing dart flutter


【解决方案1】:

您可以将标准Navigator 用作嵌套,无需任何额外技巧。

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

您唯一需要知道的是,此导航器的上下文不会是全局的。它将导致使用它的一些特定点。

下面的例子稍微复杂一些,但它可以让您了解如何从外部和内部为导航器小部件设置嵌套路由。例如,我们在根页面中调用setState 以通过NestedNavigator 的initRoute 设置新路由。

  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' for 嵌套导航器的根导航器和“/计划”并处理此数据 分开。

【讨论】:

  • 不错的解决方案!但是我注意到,在热重新加载时,嵌套导航器的状态会丢失,我将从初始路线开始,这使得开发非常烦人。有人遇到过同样的问题吗?
【解决方案2】:

虽然在技术上可以嵌套“导航器”,但此处不推荐这样做(因为它会破坏 Hero 动画)

您可以使用onGenerateRoute 构建嵌套的“路由”,如果是路由“/dashboard/profile”,则构建一棵树WidgetApp &gt; Dashboard &gt; Profile。我认为这就是您想要实现的目标。

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

为了提供代码流的线索:NestedRoute 忽略了布局的确切构建,让它在 builder 方法中(例如 builder: (child) =&gt; 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 class + 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 和Bar 组件将不会与您的路由系统紧密耦合;但仍然有嵌套路线。 它比将您的路线分发到各处更具可读性。而且您可以轻松添加一个新的。

【讨论】:

  • 我给你这个更优雅并且兼容堆栈弹出拉取。无论如何,每当我想从配置文件(仪表板/配置文件)导航到 foo(仪表板/foo)时,这段代码是否仍在重新绘制仪表板?这与 Dashboard(child: profile) 和 Dashboard(child:foo) 有两条不同的路由不一样吗?
  • 不完全是。展平WidgetApp 的routes 属性中所有可能的路由会导致大量重复。它会起作用的;但会比这种方法更难维护。尤其是在较大的路线上。
  • 另一个不同之处是您将能够拥有像/dashboard/profile/{userId} 这样的动态路由,几乎不需要修改。您甚至可以插入一个代码生成器,通过解析和类型检查来提取构造函数的参数。
  • 感谢您提供宝贵的信息。我只建议您对解释背后的理论进行一些改进,并添加一些对官方文档的参考;)
  • 哇,这很好。但是颤振文档没有说清楚我不知道这可以做到,谢谢你的时间
【解决方案3】:

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

编辑:您想要实现的行为需要使用 onGenerateRoute,但尚未(18 年 1 月)正确记录(doc)。请参阅@Darky 答案以获取示例。他提出了NestedRouteBuilder 和NestedRoute 的实现,填补了空白。

使用 MaterialApp 中的普通 Navigator,路由和页面导航(根据 doc)有两个主要特征,拒绝实现您想要实现的目标(至少直接实现)。 一方面,Navigator 的行为类似于 堆栈,因此将一个路由推入和弹出下一个,依此类推,另一方面 em> 路线要么是全屏 要么是模态 - 这意味着它们会部分占据屏幕,但它们抑制与下方小部件的交互。更明确地说,您的范例似乎需要与堆栈中不同级别的页面同时交互 - 这是无法做到的。

此外,感觉路径范式不仅是一个层次结构 - 通用框架 → 特定子页面 - 而是首先在导航器中表示堆栈。我自己也被骗了,但是阅读this就很清楚了:

String initialRoute

final

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

默认情况下,这取决于 dart:ui.Window.defaultRouteName。

如果这个字符串包含任何 / 字符,那么字符串被分割 从字符串开头到的那些字符和子字符串 每个这样的字符依次用作推送的路线。

例如,如果使用路由 /stocks/HOOLI 作为初始路由, 然后导航器将在启动时推送以下路由:/, /股票,/股票/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 动画,它们可以为您提供您在视图之间寻找的连续性。

【讨论】:

  • 这是错误的。您可以使用 onGenerateRoute 轻松拥有嵌套路由。我会在几分钟内提交一个示例
  • @Darky 我对文档进行了几天的搜索,除了发现的内容外,没有发现任何线索。我自己认为可以做到——而且应该可以轻松做到,但我没有找到一种优雅的方式。期待看到该示例,并希望他们也能尽快改进 Flutter 文档以解决这些问题。
猜你喜欢
  • 1970-01-01
  • 2022-12-11
  • 2019-12-16
  • 2021-05-02
  • 1970-01-01
  • 2021-07-03
  • 2021-08-30
  • 2021-12-07
  • 2021-05-30
相关资源
最近更新 更多