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.。问题:为什么? 您可能会犯错误,因为复制粘贴重复代码很常见而忘记更改它。在这种特殊情况下,机会更小,因为代码更简单,因此您可以轻松发现任何错误,而在更复杂的代码中,要更改的小部分可能会隐藏在所有额外的样板中。 因此,基于动态类型(路由名称为 StringargumentsObject!)复制粘贴 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 推送给构建器,对吗? contextonGenerateRoute 中可用吗?【参考方案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.dartroutes 目录。您可以在 main() 上添加评论,例如 // See routes` 目录以查找路线`,或其他内容。从我的角度来看,这些都是软问题,可以通过目录和评论来解决,而不是过度框架和无缘无故失去静态类型。 但是如果我们是哲学的,我不明白为什么导航作为副作用是错误的。从流程的角度来看,导航是用户交互的副作用。一组所有可能的路由只能静态地合理分组,通过将路由文件分组到一个文件夹中就可以完美解决。【参考方案5】:

如果您使用push(),则每次需要导航到该屏幕时都必须导入 SecondRoute 所在的文件。对于需要在不同屏幕上导航的大型项目来说,这是过多的代码重复。

如果您使用pushNamed(),您只需在您的 MaterialApp 中定义一次路由。然后,您可以从任何地方导航到任何屏幕,而无需像 push() 那样重复相同的操作。

选择PushNamed() 的另一个重要原因是能够使用它构建您自己的导航系统。您甚至可以在某些用户导航到屏幕之前决定他们是否可以使用路线。

【讨论】:

所说的“不重复同一件事”是指MaterialPageRoute(builder: (context) =>,即Route对象的构造? 我并没有真正了解“决定某些用户是否可以使用路线”部分,您能详细说明一下吗? @cubuspl42,您不需要重复MaterialPageRoute,也不需要重复import 来导入包含小部件的文件。我为我的项目创建了一个导航类,所以当我想从一个屏幕导航到另一个屏幕时,我会检查用户状态。如果用户未登录,它将导航到登录屏幕。所以,我不需要在每个屏幕上检查它。除此之外,我将所有页面路由转换类型放在同一个类中,这样我就可以轻松管理它们。但是,使用Navigator.push(),我将无法做到这一点。 我不相信教程作者,或者说老实说的任何人,实际上认为导入是代码重复。在这些日子里,它们是自动管理的,并提供比成本更多的好处。在过去,#includes 的“代码重复”较少,因为一个“行”将大量符号引入全局范围。没有人对此感到满意。 导航和“条件路由”参数听起来很有效,这是another answer 的要点之一。您可以检查 cmets。【参考方案6】:

为了理解为什么我们应该使用 Navigator.pushNamed 而不是 Navigator.push,首先让我们熟悉一下 Navigator 方法。你有没有想过Navigator.popUntil或Navigator.pushAndRemoveUntil? 当我们想在堆栈中弹出到特定路由时,我们使用 Navigator.popUntil。如果您查看文档,您会发现将这些方法与 pushNamed 方法一起使用非常容易。另外,检查documentation 中的所有方法。当我尝试理解 Flutter 中的路由时,this 文章对我非常有用。 作为一个缺点,在这种方法中处理参数非常困难。您应该为每条路线创建 onGenerateRoute 并处理参数。

【讨论】:

以上是关于Flutter 中的命名路由如何消除重复?的主要内容,如果未能解决你的问题,请参考以下文章

普通路由普通路由传值 命名路由命名路由传值

未定义命名参数“home” - Flutter

Flutter之基本路由,命名路由跳转,返回上一页,替换路由和返回根路由——Flutter基础系列

如何在颤动的BottomNavigationBar中使用命名路由?

Flutter 12 个小技巧

Flutter 中的路由