Flutter 中的命名路由如何消除重复?
Posted
技术标签:
【中文标题】Flutter 中的命名路由如何消除重复?【英文标题】:How named routes in Flutter eliminate duplication? 【发布时间】:2019-08-23 03:53:09 【问题描述】:我不明白为什么有人应该使用Navigator.pushNamed()
的命名路由,而不是Navigator.push()
的正常 方式。
tutorial page 声明:
如果我们需要在应用的许多部分导航到同一个屏幕, 这可能会导致代码重复。在这些情况下,它可以派上用场 定义“命名路由”,并将命名路由用于导航
复制
使用简单路由时如何产生重复,使用命名路由如何消除重复?
我不明白这有什么区别
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
来自
Navigator.pushNamed(context, '/second');
在重复的上下文中。
【问题讨论】:
我很想听听命名路由的一些实际参数。我想在网络上,它们很重要,因为您实际上需要支持手动输入的 URL。在 Flutter(在移动设备上)中,它们只会给你更多的活力(不再对路由参数进行类型检查)和路由声明的集中化(我看不出这有什么好处!)。不过,我可能遗漏了一些重要的东西。 可能是因为这个MaterialPageRoute(builder: (context) => ...
部分重复?
【参考方案1】:
考虑在许多小部件中使用Navigator.push()
:
// inside widget A:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
// inside widget B:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
// inside widget C:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
现在假设您需要更改您的应用程序并且小部件SecondRoute
需要在其构造函数上接收一个值。现在您遇到了一个问题,因为您在多个位置有相同代码的多个副本,您需要确保更新所有这些副本,这可能很乏味且容易出错:
// inside widget A:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(
title: 'Title A',
)),
);
// inside widget B:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(
title: 'Title B',
)),
)),
);
// inside widget C:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute(
title: 'Title A', // ERROR! Forgot to change the variable after a copy/paste
)),
)),
);
现在让我们考虑使用命名路线。
首先,我绝不会建议任何人直接使用该名称进行导航,而是使用static
变量引用,这样如果您将来需要更改它,它的方式会更简单、更安全,因为您不能忘记在任何地方更新它,像这样:
class Routes
static const String second = '/second';
另一种方法是在路由本身内部有一个引用,在SecondRoute
内部有一个static const String
,所以我们可以将它用作SecondRoute.routeName
。这是 IMO 个人喜好的问题。
然后您的小部件将使用以下方式导航:
// inside widget A:
Navigator.pushNamed(context, Routes.second); // Routes.second is the same as '/second'
// inside widget B:
Navigator.pushNamed(context, Routes.second);
// inside widget C:
Navigator.pushNamed(context, Routes.second);
现在,如果您需要在创建时将参数传递给 SecondRoute
,您可以使用 MaterialApp
onGenerateRoute
在集中位置执行此操作,例如 tutorial explains in more detail。您的代码将更改为:
// inside widget A:
Navigator.pushNamed(context, Routes.second, arguments: 'Title A');
// inside widget B:
Navigator.pushNamed(context, Routes.second, arguments: 'Title B');
// inside widget C:
// You can still make mistakes here, but the chances are smaller.
Navigator.pushNamed(context, Routes.second, arguments: 'Title C');
MaterialApp(
onGenerateRoute: (settings)
if (settings.name == Routes.second)
final String title = settings.arguments;
return MaterialPageRoute(
builder: (context) => SecondRoute(title: title),
);
,
);
重复代码的数量减少了,但另一方面,onGenerateRoute
代码会随着您创建更多路线而变得更加复杂,因为它们的所有创建都将集中在那里,所以恕我直言,这更多的是个人喜好而不是一般准则。
【讨论】:
感谢您的回答。它很详细,代码 sn-ps 有助于理解您的观点。虽然我明白你在说什么,但我绝对不明白结论。现在,我只问一个问题。在您的一个 sn-ps 中,您添加了一条评论:// You can still make mistakes here, but the chances are smaller.
。问题:为什么?
您可能会犯错误,因为复制粘贴重复代码很常见而忘记更改它。在这种特殊情况下,机会更小,因为代码更简单,因此您可以轻松发现任何错误,而在更复杂的代码中,要更改的小部分可能会隐藏在所有额外的样板中。
因此,基于动态类型(路由名称为 String
和 arguments
为 Object
!)复制粘贴 67 个字符长的 sn-p 可能会导致不同步(您可以通过意外将用于B
的参数传递给路由A
)sn-p 比复制粘贴 97 个字符长的 sn-p 更危险,后者是完全静态类型的(调用构造函数)并且没有失同步的可能性(参数是通过静态类型绑定到构造函数名称)?
另外,较长的有可能使用扩展方法等标准工具缩短到 69:Navigator.of(context).pushM((ctx) => SecondRoute(title: 'Title A'));
。
正如我所说,我认为这是基于意见的,每个人都会对此有感觉。如果您认为您从 Google 阅读的文章需要进一步澄清,顶部有一个按钮可让您打开错误报告,您可以在此处要求他们进一步澄清。【参考方案2】:
Push 和 PushNamed 的作用类似,Push 会切换到你指定的路由,而 PushNamed 会切换到指定路由名的路由。
教程页面对重复的含义是代码的重复而不是路由的重复。
例如,您有一条路线,您想检查用户是否已登录并显示相应的页面
仅使用推送: 第 1 页:
//This is page 1....
RaisedButton(
child: Text('Go to second'),
onPressed: ()
if (user.state = "login")
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPage(),
),
)
else
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPageAnonymous(),
),
)
)
....
在另一个页面 Page2 中,您需要重复相同的代码:
//This is page 2....
RaisedButton(
child: Text('Go to second'),
onPressed: ()
if (user.state = "login")
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPage(),
),
)
else
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondPageAnonymous(),
),
)
)
....
使用 PushNamed,您只需声明一次,基本上可以一遍又一遍地重用它。
在你的 onGenerateRoute 中:
onGenerateRoute: (settings)
switch (settings.name)
case '/':
return MaterialPageRoute(builder: (_) => FirstPage());
case '/second':
if (user.state = "login")
return MaterialPageRoute(
builder: (_) => SecondPage()
);
else
return MaterialPageRoute(
builder: (_) => SecondPageAnonymous()
);
default:
return _errorRoute();
,
现在在您项目的任何页面中,您都可以这样做:
Navigator.of(context).pushNamed('/second')
无需在每次使用时重复检查登录甚至错误处理。显而易见的好处是,您可以通过防止重复代码片段,而不是一次又一次地重复,从而在整个应用程序中保持一致。
现在,但这并不能防止路线重复!在这种情况下 push 和 pushNamed 没有区别!
但是由于您的路线现在已命名,您可以轻松地执行 popUntil('/') 以返回到路线的第一个实例,而不是再次创建它或 PushReplacementNamed。
【讨论】:
这是一个很好的观察。好吧,也许教程作者的意思是,因为他们没有这样设计他们的示例。我喜欢使用最简单的工具来解决给定的问题。对于添加两个数字,我使用 + 等。让我们从未命名的路线开始:Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage())))
。 问题:嗯,实际上这对于登录用户应该是SecondPage
,对于其他用户应该是SecondPageAnonymous
...我将这个抽象称为second
。 解决方案:Navigator.of(context).push(MaterialPageRoute(builder: (context) => second())))
当然,这里的second
是一个辅助函数,它和你的sn-p做同样的切换。我假设给定了user
对象,可能是全局的。顺便说一句,如果应用程序状态未存储在全局变量中,我不知道您的整个路由策略是否有效。
如果我想删除 MaterialPageRoute(builder: (context) =>
重复,我会在 Navigator.pushM(RouteBuilder builder)
上声明一个扩展方法,所以它就像 Navigator.of(context).push((ctx) => MyRoute1())
一样使用。我是疯了,还是世界疯了吗?
对于全球,您可能需要查看 Providers。这里我只是举个例子,假设你的用户在全局空间中。
对于提供者,您需要将if
推送给构建器,对吗? context
在onGenerateRoute
中可用吗?【参考方案3】:
我可以看到使用 Navigate with named routes 的唯一优势是在 MaterialApp 中声明了路由,因此开发人员只能使用分配的路由,即小部件、页面,
如果有其他人使用,会报错'onUnknownRoute is called.'
【讨论】:
所以第一个好处是限制了开发人员(虽然不是真的,因为他们总是可以......只是添加新的路线,到 MaterialApp 或者然而;这是他们的应用程序)。第二个好处是 runtime 错误,而不是未命名路由的编译时错误。 运行时错误如何优于编译时错误?【参考方案4】:这是我的初学者颤振想法:
它使代码更简洁: 如果不在更高级别的小部件上声明路由,新屏幕会突然出现,以响应应用程序中发生的任何事情。当您一起声明路线时,更容易理解导航骨架/结构,在更高的小部件中更容易理解,尤其是对于其他开发人员。当然,这不有助于准确了解这些路线实际导航到的时间,但它是一个小的改进,并将我们带回到声明性范式。声明的路由提供的提示将帮助新开发人员了解您的导航流程。
【讨论】:
这样你就可以很容易地了解项目中使用的路由集合,并带有命名路由。这很酷,但您也可以通过许多其他方式做到这一点,而无需所有的动态和框架结构,例如通过在您的项目中拥有一个routes
目录并将您的路由文件放在那里。然后,您还可以在路由符号上使用 IDE Find Usages
功能。这里甚至不是平局,未命名的路线是 1:0。
顺便说一句,“未命名”听起来像是一个贬义词。就像......这些路线没有名字,我们都知道 name 是赋予身份的东西。他们确实有名字。在OP给出的示例中,路由的名称是SecondRoute
。这是一个伟大的名字。
如果你有一个路由文件,它只会在Navigator.push
被调用时被导入/使用。所以当你查看应用程序的整体结构时,从main
开始往下走,你不会看到任何关于导航的信息,所有的导航都只是按键的副作用,而这些路由从来没有之前已经声明过。如果我正在查看一个项目,出于这个原因,我希望在创建正在更改的小部件(路由)的小部件上声明导航。
这是一个近乎哲学的推理。当您从main()
开始向下移动时,您还会在它旁边看到main.dart
和routes
目录。您可以在 main()
上添加评论,例如 // See
routes` 目录以查找路线`,或其他内容。从我的角度来看,这些都是软问题,可以通过目录和评论来解决,而不是过度框架和无缘无故失去静态类型。
但是如果我们是哲学的,我不明白为什么导航作为副作用是错误的。从流程的角度来看,导航是用户交互的副作用。一组所有可能的路由只能静态地合理分组,通过将路由文件分组到一个文件夹中就可以完美解决。【参考方案5】:
如果您使用push()
,则每次需要导航到该屏幕时都必须导入 SecondRoute 所在的文件。对于需要在不同屏幕上导航的大型项目来说,这是过多的代码重复。
如果您使用pushNamed()
,您只需在您的 MaterialApp 中定义一次路由。然后,您可以从任何地方导航到任何屏幕,而无需像 push()
那样重复相同的操作。
选择PushNamed()
的另一个重要原因是能够使用它构建您自己的导航系统。您甚至可以在某些用户导航到屏幕之前决定他们是否可以使用路线。
【讨论】:
所说的“不重复同一件事”是指MaterialPageRoute(builder: (context) =>
,即Route
对象的构造?
我并没有真正了解“决定某些用户是否可以使用路线”部分,您能详细说明一下吗?
@cubuspl42,您不需要重复MaterialPageRoute
,也不需要重复import
来导入包含小部件的文件。我为我的项目创建了一个导航类,所以当我想从一个屏幕导航到另一个屏幕时,我会检查用户状态。如果用户未登录,它将导航到登录屏幕。所以,我不需要在每个屏幕上检查它。除此之外,我将所有页面路由转换类型放在同一个类中,这样我就可以轻松管理它们。但是,使用Navigator.push()
,我将无法做到这一点。
我不相信教程作者,或者说老实说的任何人,实际上认为导入是代码重复。在这些日子里,它们是自动管理的,并提供比成本更多的好处。在过去,#include
s 的“代码重复”较少,因为一个“行”将大量符号引入全局范围。没有人对此感到满意。
导航和“条件路由”参数听起来很有效,这是another answer 的要点之一。您可以检查 cmets。【参考方案6】:
为了理解为什么我们应该使用 Navigator.pushNamed 而不是 Navigator.push,首先让我们熟悉一下 Navigator 方法。你有没有想过Navigator.popUntil或Navigator.pushAndRemoveUntil? 当我们想在堆栈中弹出到特定路由时,我们使用 Navigator.popUntil。如果您查看文档,您会发现将这些方法与 pushNamed 方法一起使用非常容易。另外,检查documentation 中的所有方法。当我尝试理解 Flutter 中的路由时,this 文章对我非常有用。 作为一个缺点,在这种方法中处理参数非常困难。您应该为每条路线创建 onGenerateRoute 并处理参数。
【讨论】:
以上是关于Flutter 中的命名路由如何消除重复?的主要内容,如果未能解决你的问题,请参考以下文章
Flutter之基本路由,命名路由跳转,返回上一页,替换路由和返回根路由——Flutter基础系列