一尘不染的路由和导航

介绍 (Intro)

As soon as you’ve finished studying the basics of Flutter and start working on your first projects, you’ll find yourself asking:

学习完Flutter的基础知识并开始进行第一个项目后,您会发现自己在问:

“What’s the best way I can handle routing and navigation?”

“处理路线和导航的最佳方法是什么?”

“Should I use named routes? Should I use a package?”

“我应该使用命名路线吗? 我应该使用包裹吗?”

No, I’m not here to say “it depends on your needs.”

不,我不是在这里说“这取决于您的需求”。

I work at a software service company, and as such, we develop many apps a year. That puts a demand on finding solutions that fit the largest number of different projects.

我在一家软件服务公司工作,因此,我们每年开发许多应用程序。 这就要求寻找最适合不同项目的解决方案。

If you’ve done native mobile development, you know that routing always becomes a serious matter. Whenever I’m around iOSers, it’s five minutes before we’re discussing routers, coordinators, flows, and their pros and cons. With Androiders, it’s exactly the same — just replace the terms with Jetpack Navigation, Cicerone or FragNav. It seems like no one is ever completely satisfied with it.

如果您已经完成了本机移动开发,那么您将知道路由总是很重要的事情。 每当我和iOS使用者在一起时,距离我们讨论路由器,协调器,流量及其优缺点只有五分钟的时间。 对于Androiders,这是完全一样的-只需用Jetpack Navigation,Cicerone或FragNav替换条款即可。 似乎没有人对此完全满意。

If Flutter is your first foray into the mobile world, remember these words: This won’t be the last time you talk about routing strategies.

如果Flutter是您首次涉足移动世界,请记住以下几句话:这不会是您最后一次谈论路由策略了。

And before you get hopeless about it, just give me a chance to show you how I’ve been doing it.

在您对此毫无希望之前,请给我一个机会向您展示我的工作方式。

I’m completely satisfied and I think you will be too.

我完全满意,我想你也一定会满意。

Have you ever taken time to think about how navigation is typically made on the web?

您是否曾经花时间考虑过通常如何在网络上进行导航?

I’m not talking about how it’s developed or anything like that — I’m interested in your experience as a website user or RESTful API consumer.

我不是在谈论它是如何开发的或类似的东西—我对您作为网站用户或RESTful API使用者的经验很感兴趣。

URLs are typically designed around resources, with a resource being a collection or a single item. For example:

URL通常是围绕资源设计的,资源是集合或单个项目。 例如:

Want to access a list of movies? Go to ${yourFavoriteMovieWebsite}/movies.

要访问电影列表吗? 转到${yourFavoriteMovieWebsite}/movies

Want to see more details about a specific movie you found on the list? Go to ${yourFavoriteMovieWebsite}/movies/:picked_movie_id.

是否想查看有关您在列表中找到的特定电影的更多详细信息? 转到${yourFavoriteMovieWebsite}/movies/:picked_movie_id

Did you notice the parametrized ID? That’s a path param or URL param.

您是否注意到参数化的ID? 这是路径参数或URL参数。

What about seeing only the movies released in 2020? No problem, just pass in a query param called releaseYear to the first endpoint:

只看2020年上映的电影怎么办? 没问题,只需将名为releaseYear的查询参数releaseYear给第一个端点:

${yourFavoriteMovieWebsite}/movies?releaseYear=2020

${yourFavoriteMovieWebsite}/movies?releaseYear=2020

By the way, that part of the URL containing the parameter, starting with the question mark, is what we call a query string.

顺便说一下,URL中包含参数的那一部分,从问号开始,就是我们所说的查询字符串。

Further reading: REST Resource Naming.

进一步阅读: REST资源命名

如果我们可以对我们的应用程序做同样的事情怎么办? (What if We Could Do the Same With Our Apps?)

“Why would I want to do such a thing?”

“我为什么要做这样的事情?”

Apart from it being amazingly intuitive and organized, we’re just one step behind being ready for deep links.

除了非常直观和井井有条之外,我们仅比准备进行深层链接落后了一步。

“Deep links?”

“深层链接?”

Have you ever opened a link on your phone that, instead of opening a web page, opens a specific page of an app?

您是否曾经在手机上打开过链接,而不是打开网页,而是打开了应用程序的特定页面?

Another good example is when you receive a push notification from a messenger app and tapping on it, it takes you to that specific conversation screen.

另一个很好的例子是,当您从Messenger应用收到推送通知并点击该通知时,它将带您进入特定的对话屏幕。

That’s what deep linking is all about. That’s also all we’re saying about it for today — it will have an article of its own soon.

这就是深层链接的全部内容。 这就是我们今天要说的所有内容-很快它将有一篇自己的文章。

My point is this: If we’re going to think of URL paths to our pages sooner or later, why not do it from the beginning? Even if you don’t have any intention of supporting deep links in your project, this is still the most well-established way of organizing routes.

我的意思是:如果我们迟早会想到页面的URL路径,为什么不从一开始就这么做呢? 即使您不打算在项目中支持深层链接,这仍然是组织路线的最完善的方法。

To showcase today’s article, I decided we needed to work on a real app. To make it possible, I spent some time looking for free HTTP APIs, that’s when I found the great Breaking Bad API.

为了展示今天的文章,我决定我们需要开发一个真实的应用程序。 为了使之成为可能,我花了一些时间寻找免费的HTTP API,那时我发现了出色的Breaking Bad API

It provides us with a collection of information on the Breaking Bad and Better Call Saul TV Shows. Our app, though, is restricted to Breaking Bad characters and its remarkable quotes.

它为我们提供了有关“打破常规”和“更好的呼叫扫罗”电视节目的信息。 但是,我们的应用仅限于Breaking Bad字符及其引号。

That settled, I found myself with the most challenging task of it all. Deciding what the app’s name would be!

解决之后,我发现自己承担了所有挑战中最艰巨的任务。 确定应用程序的名称!

断断续续的Bapp出生了 (The Breaking Bapp Was Born)

Don’t worry about my marketer skills — I don’t intend to follow that career! Also, I promise you I’ll do my best to keep the “breaking” part only in the name.

不用担心我的营销技巧-我不打算从事该职业! 另外,我向您保证,我会尽力将“中断”部分保留在名称中。

Breaking Bapp walkthrough
Breaking Bapp walkthrough
突破Bapp演练

资源思考 (Thinking in Resources)

We already stated that we should design our paths around the resources we’re providing. Can you imagine what the URLs would look like if we were talking about a website? I did that exercise:

我们已经说过,我们应该围绕所提供的资源设计路径。 您能想象如果我们谈论一个网站时URL会是什么样? 我做了那个练习:

  • List of characters: characters

    字符列表: characters

  • Details of a single character: characters/:character_id

    单个字符的详细信息: characters/:character_id

  • List of quotes: quotes

    报价清单: quotes

  • Details of a quote author: quotes/authors?name=:quote_author_name

    报价作者的详细信息: quotes/authors?name=:quote_author_name

The last one is the trickiest. Maybe you’re wondering why name is a query param instead of a path param, or why we aren’t using the quote’s author ID instead of their name.

最后一个是最棘手的。 也许您想知道为什么name是查询参数而不是路径参数,或者为什么我们不使用引号的作者ID而不是名称。

To answer that, let me first show you a quote’s JSON object retrieved from the API:

为了回答这个问题,让我首先向您展示从API检索到的报价的JSON对象:

{
"quote_id":1,
"quote":"I am not in danger, Skyler. I am the danger!",
"author":"Walter White"
}

Notice that we don’t have the ID of the author, just its name. Since name isn't a reliable unique identifier and path params are recommended for unique identifiers, the best thing we can do is thinking as if we're querying the author's resource.

请注意,我们没有作者的ID,只有名字。 由于name不是可靠的唯一标​​识符,因此建议为唯一标识符建议使用路径参数,因此,我们最好的办法就是考虑要查询作者的资源。

By designing it like this, I can teach you how to work with query strings as we go.

通过这样设计,我可以教您如何使用查询字符串。

It’s important to mention, for those of you who are not Breaking Bad fans, that a quote author is nothing but a character of the show. So, to obtain this info, we just need to use the character details endpoint — fortunately, the API allows us to find a character by its name instead of its ID.

值得一提的是,对于那些不是Breaking Bad粉丝的人来说,报价作者不过是节目的角色。 因此,要获取此信息,我们只需要使用字符详细信息终结点-幸运的是,API允许我们通过字符名称而不是ID查找字符。

导航如何在Flutter中工作? (How Does Navigation Work in Flutter?)

Navigating is nothing but the simple act of transitioning to a new full-screen widget, which we call a page or a screen. To do so, we need to wrap this new widget inside a Route object, that describes how the transition should happen, and push it to a Navigator. There are two ways to do this, by default.

导航只不过是过渡到新的全屏小部件(我们称为pagescreen)的简单操作 。 为此,我们需要将此新的小部件包装在Route对象中,该对象描述过渡的发生方式,并将其推送到Navigator 。 默认情况下,有两种方法可以执行此操作。

第一种方法 (First method)

Give the Navigator a Route object.

Navigator一个Route对象。

Bringing it to the Breaking Bapp, this is how it would look like for the onTap callback of the character’s list:

将其带入Breaking Bapp,这是角色列表的onTap回调的外观:

final destinationRoute = MaterialPageRoute(
  builder: (context) => CharacterDetailPage(
    id: character.id,
  ),
);


Navigator.push(
  context,
  destinationRoute,
);

(Pro) Extremely easy to learn;

(专业版)非常容易学习;

(Pro) Strongly typed;

(赞成)强类型;

(Con) Leads to lots of code duplication if you need to navigate to the same screen in many parts of your app;

(Con)如果您需要导航到应用程序许多部分的同一屏幕,则会导致大量代码重复;

第二种方法,又名路由 (Second method, a.k.a. named routes)

Give the Navigator a route name and a generic argument (optional), which will be delivered to a function that maps these two to a Route.

Navigator一个路由名称和一个通用参数(可选),该参数将传递给将这两个映射到Route的函数。

You need to write that function by yourself and use it as the onGenerateRoute of the MaterialApp.

您需要自己编写该函数并将其用作MaterialApponGenerateRoute

// This is how we push a new page using named routes. It could be called 
// when the user selects an item from the character list.
void pushNewPage(BuildContext context, CharacterSummary character) {
  Navigator.pushNamed(
    context,
    'character-details',
    // Since we use the same Navigator.pushNamed 
    // function to every page we wanna push, the below `arguments` 
    // parameter type is `Object`, and there's nothing 
    // forbidding me to give it a `boolean` where we would 
    // actually be expecting an int, i.e., the character's id.
    arguments: character.id,
  );
}


// This is the definition of the MyApp Widget, which can be found
// at the main.dart file. Notice the MaterialApp's onGenerateRoute constructor
// parameter.
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Named Routes',
        theme: ThemeData(
          primarySwatch: Colors.green,
        ),
        home: HomeScreen(),
        // It's a function that receives a RouteSettings object and returns
        // a concrete Route. The RouteSettings contains information about the
        // route we're intending to navigate to, such as its name and any 
        // passed arguments. If we return nothing here, it will call the  
        // onUnknownRoute function, which we're not implementing here, but 
        // could be used for showing a default page, for example.
        onGenerateRoute: (routeSettings) {
          if (routeSettings.name == 'character-details') {
            final int characterId = routeSettings.arguments;
            return MaterialPageRoute(
              builder: (context) => CharacterDetailPage(
                id: characterId,
              ),
            );
          }


          return null;
        },
      );
}

(Con) Not as easy to learn as the first method.

(缺点)不如第一种方法容易学习。

(Pro) Although it looks more verbose, it is actually the recommended approach, because we keep our page’s instantiation centralized, avoiding code duplication.

(Pro)尽管看起来比较冗长,但实际上是推荐的方法,因为我们将页面的实例化保持在集中状态,避免了代码重复。

If we take a closer look, the named route approach is relatively close to the web-thing we’re trying to accomplish. The only difference is that on the web, we wouldn’t need this argument object (the third param of the Navigator.pushNamed), as we would be able to pass every parameter we need inside the route name, either as path param or as a query param.

如果我们仔细研究一下,命名路由方法相对接近于我们要完成的Web工作。 唯一的区别是,在网络上,我们就不需要这种说法对象(第三PARAM Navigator.pushNamed ),因为我们能够通过我们每次需要的航线名称内部参数,无论是作为路径PARAM或查询参数。

We want to do this: Navigator.of(context).pushNamed('characters/${character.id}');

我们要这样做: Navigator.of(context).pushNamed('characters/${character.id}');

Instead of this: Navigator.of(context).pushNamed('character-details', arguments: character.id);

取而代之的是: Navigator.of(context).pushNamed('character-details', arguments: character.id);

However, if you try to do this right away, it will give you a headache. Inside your onGenerateRoute function, you would need a way to wildcard the character's ID, so that characters/11 and characters/22 would take to the same route, just with different arguments. Another problem is having to strip and parse whatever comes after the question mark as query parameters.

但是,如果您尝试立即执行此操作,则会使您头痛。 在onGenerateRoute函数内部,您将需要一种通配符ID的方式,以便characters/11characters/22将采用相同的路由,只是带有不同的参数。 另一个问题是必须剥离和解析问号后面的所有内容作为查询参数。

Fluro救援包 (Fluro Package to the Rescue)

According to them (and I’m inclined to agree), Fluro is:

根据他们的看法(我倾向于同意),Fluro是:

The brightest, hippest, coolest router for Flutter.

Flutter最亮,最时髦,最酷的路由器。

Among several features, I would highlight:

在几个功能中,我要强调:

  • Wildcard parameter matching

    通配符参数匹配
  • Querystring parameter parsing

    查询字符串参数解析

We found our guy!

我们找到了我们的家伙!

For our starting point, I built a version of the Breaking Bapp using the first routing method (without named routes), from which we’ll evolve to an elegant solution using Fluro.

首先,我使用第一种路由方法(没有命名的路由)构建了Breaking Bapp版本,从此方法我们将逐步发展为使用Fluro的优雅解决方案。

Don’t worry about how I made the HTTP calls or what is the state management approach. I’ve managed to keep things as simple as possible so that it won’t take the focus of the navigation and routing.

不用担心我如何进行HTTP调用或什么是状态管理方法。 我设法使事情尽可能简单,以免将重点放在导航和路由上。

To cover a scenario with multiple Navigators, we have a bottom navigation menu structure.

为了涵盖具有多个Navigator的场景,我们有一个底部的导航菜单结构。

I suggest you check out the starting-point branch of the Breaking Bapp's GitHub repository and get relatively comfortable with it for a better learning experience.

我建议您检查Breaking Bapp的GitHub存储库starting-point分支,并对其比较满意,以获得更好的学习体验。

逐步迁移到Fluro (Step-by-step on Migrating to Fluro)

1.安装Fluro (1. Install Fluro)

The article is based on version 1.6.3.

本文基于版本1.6.3。

Please, don’t use a newer version until you’re finished with all the steps. I can’t state how important that is.

在完成所有步骤之前, 请不要使用较新的版本 。 我不能说这有多重要。

https://pub.dev/packages/fluro#-installing-tab-

https://pub.dev/packages/fluro#-installing-tab-

2.设置路线 (2. Set up your routes)

The main Fluro’s class is called Router. We’ll use it for everything we need to do, except for pushing pages, as we’ll continue to use the Navigator for that. You don’t need to instantiate it by yourself, as Fluro already made an instance available to us as a static variable, simply accessed by Router.appRouter.

Fluro的主要类称为Router 。 我们将使用它来完成除推送页面之外的所有工作,因为我们将继续使用Navigator 。 您不需要自己实例化它,因为Fluro已经将实例作为静态变量提供给我们,只需通过Router.appRouter访问Router.appRouter

First things first, we’ll start using a Router’s function called define, which serves the purpose of, guess what, defining a route. It takes in three arguments:

第一件事首先,我们将开始使用Router的函数调用define ,供应的,你猜怎么目的, 定义的路由。 它包含三个参数:

  • A String representing the route’s path along with its expected path parameters. The query ones don’t need to be specified ahead.

    一个字符串,代表路线的路径及其预期的路径参数。 不需要预先指定查询的内容。

  • A handler object which holds the page’s widget builder.

    一个处理程序对象,用于保存页面的窗口小部件生成器。
  • An optional TransitionType, just in case you want to change the default route transition.

    可选的TransitionType ,以防万一您想更改默认路由转换。

I did all my routing set up inside the main function (main.dart), and I suggest you do the same.

我在主函数( main.dart )中完成了所有路由设置,建议您也这样做。

Read the Gist below carefully. I made an effort to comment everything you need to know.

请仔细阅读以下要点。 我努力评论了您需要知道的所有内容。

void main() {
  Router.appRouter
    // The '..' syntax is a Dart feature called cascade notation.
    // Further reading: https://dart.dev/guides/language/language-tour#cascade-notation-
    ..define(
      // The '/' route name is the one the MaterialApp defaults to as our initial one.
      '/',
      // Handler is a custom Fluro's class, in which you define the route's
      // widget builder as the Handler.handlerFunc.
      handler: Handler(
        handlerFunc: (context, params) => HomeScreen(),
      ),
    )
    ..define(
      'characters',
      handler: Handler(
        handlerFunc: (context, params) => CharacterListPage(),
      ),
    )
    ..define(
      // The ':id' syntax is how we tell Fluro to parse whatever comes in
      // that location and give it a name of 'id'. This is called a Path Param 
      // or URI Param.
      'characters/:id',
      transitionType: TransitionType.native,
      handler: Handler(
        handlerFunc: (context, params) {
          // The 'params' is a dictionary where the key is the name we gave to
          // the parameter ('id' in this case), and the value is an array with
          // all the arguments that were provided (just a single `int` in this
          // case). Fluro gives us an array as the value instead of a single
          // item because when we're working with query string parameters,
          // we're able to pass an array as the argument, such as
          // '?name=Jesse,Walter,Gus'.
          final id = int.parse(params['id'][0]);
          return CharacterDetailPage(
            id: id,
          );
        },
      ),
    )
    ..define(
      'quotes',
      handler: Handler(
        handlerFunc: (context, params) => QuoteListPage(),
      ),
    )
    ..define(
      // This route will accept a Query Param, but notice that, unlike we did 
      // with Path Params, we don't need to pre-define our expected Query 
      // Params in the path String.
      'quotes/authors',
      // You can customize the transition type for every route.
      transitionType: TransitionType.nativeModal,
      handler: Handler(
        handlerFunc: (context, params) {
          // We extract an expected Query Param just as we did with
          // the 'id' in the third route definition. The only difference being
          // that with Query Params we didn't need to specify it in the 
          // 'quotes/authors' route path.
          final name = params['name'][0];
          return CharacterDetailPage(
            name: name,
          );
        },
      ),
    );


  runApp(
    MyApp(),
  );
}

3.将您的路由器链接到导航器 (3. Link your router to your navigators)

Now we have a special little guy called Router.appRouter that knows how to handle all our web-like named routes. But we haven't plugged it anywhere. Because of that, when we Navigator.pushNamed something, it still won't use all the setup we just did in the previous step.

现在,我们有一个叫Router.appRouter的特殊小家伙,他知道如何处理所有类似于网络的命名路由。 但是我们还没有在任何地方插入它。 因此,当我们对Navigator.pushNamed命名时,它仍然不会使用我们在上一步中所做的所有设置。

Do you remember the MaterialApp.onGenerateRoute constructor parameter? We've talked about it briefly when discussing named routes. We need to provide it with a function that receives the pushed route settings and uses it to build a concrete Route. Guess who has one of these? Yes, the Router.appRouter.

您还记得MaterialApp.onGenerateRoute构造函数参数吗? 在讨论命名路由时,我们已经简短地讨论了它。 我们需要为其提供一个函数,该函数可以接收推送的路由设置并使用它来构建具体的Route 。 猜猜谁拥有其中之一? 是的, Router.appRouter

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Routing, navigation and deep linking sample',
        theme: ThemeData(
          primarySwatch: Colors.green,
        ),
        onGenerateRoute: (settings) => Router.appRouter
            .matchRoute(context, settings.name, routeSettings: settings)
            .route,
      );
}

Now, every time we navigate using our root Navigator, everything works just fine.

现在,每次我们使用 Navigator ,一切都很好。

“What do you mean by root Navigator?”

“根导航器是什么意思?”

By default, inside MaterialApp, we're given a Navigator to push and pop our pages widgets, and this is the one we've just linked with our Router. But if your navigation structure is more complicated than that, for example if you use a bottom navigation component, you probably have more than one Navigator. We need to link those to Fluro as well. Fortunately that's the Breaking Bapp's scenario!

默认情况下,在MaterialApp内部,我们得到了一个Navigator来推送和弹出页面小部件,而这正是我们刚刚与Router链接的那个。 但是,如果导航结构比这更复杂,例如,如果使用底部导航组件 ,则可能有多个Navigator 。 我们还需要将它们链接到Fluro。 幸运的是,这就是Breaking Bapp的场景!

“How can I make Fluro work with multiple Navigators?"

“如何使Fluro与多个 Navigator 一起使用 ?”

Short answer: The Navigator‘s constructor also has an onGenerateRoute. Just give it the same Router.appRouter.matchRoute(context, settings.name, routeSettings: settings).route everywhere you instantiate a new Navigator in your code, use the initialRoute parameter for changing its initial route, and skip to our fourth step.

简短答案Navigator的构造函数还具有onGenerateRoute 。 只要给它相同的Router.appRouter.matchRoute(context, settings.name, routeSettings: settings).route ,就可以在代码中实例化一个新的Navigator任何地方,使用initialRoute参数更改其初始路由,然后跳到第四步。

Long answer: For this, I’m going to show you how I did it in the Breaking Bapp’s bottom navigation.

长答案 :为此,我将在Breaking Bapp的底部导航中向您展示如何做到这一点。

If you haven’t followed my latest article, it won’t be easy for you to understand what’s going on. If that’s the case, I strongly suggest you be satisfied with the short answer and skip to the fourth step. I promise you there will be no losses!

如果您没有关注我的最新文章 ,那么您将不容易理解正在发生的事情。 如果是这样,我强烈建议您对简短的答案感到满意,然后跳到第四步。 我保证你不会有任何损失!

3.1. Linking your router to your additional navigators

3.1。 将路由器链接到其他导航器

Our code has a class called BottomNavigationTab that holds all the dependencies for building our app's bottom navigation flows. That includes the item to be displayed in the menu, the GlobalKey it should use for that specific tab's Navigator, and the initial page builder for each. The initialPageBuilder was useful once when we were instantiating the routes ourselves (by using the first navigation method). But now that we're using named routes, we don't like page builders anymore, just route names, as Fluro is the one building things now.

我们的代码具有一个称为BottomNavigationTab的类,该类包含用于构建应用程序底部导航流程的所有依赖项。 其中包括要在菜单中显示的项目,用于该特定选项卡的NavigatorGlobalKey以及每个项目的初始页面构建器。 当我们自己实例化路线时(使用第一种导航方法), initialPageBuilder很有用。 但是现在我们使用的是命名路由,我们不再喜欢页面构建器,而不再是页面名称,因为Fluro是现在正在构建的东西。

Here’s the diff:

这是差异:

class BottomNavigationTab {
const BottomNavigationTab({
@required this.bottomNavigationBarItem,
@required this.navigatorKey,
- @required this.initialPageBuilder,
+ @required this.initialRouteName,
}) : assert(bottomNavigationBarItem != null),
assert(navigatorKey != null),
- assert(initialPageBuilder != null);
+ assert(initialRouteName != null);
final BottomNavigationBarItem bottomNavigationBarItem;
final GlobalKey<NavigatorState> navigatorKey;
- final WidgetBuilder initialPageBuilder;
+ final String initialRouteName;
}

As we just changed the BottomNavigationTab's definition, we need to fix the place we're instantiating it:

当我们刚刚更改BottomNavigationTab的定义时,我们需要修复实例化它的位置:

class HomeScreen extends StatefulWidget {@override
_HomeScreenState createState() => _HomeScreenState();
}class _HomeScreenState extends State<HomeScreen> {
final List<BottomNavigationTab> _bottomNavigationTabs = [
BottomNavigationTab(
bottomNavigationBarItem: BottomNavigationBarItem(
title: const Text('Characters'),
icon: Icon(Icons.people),
),
navigatorKey: GlobalKey<NavigatorState>(),
- initialPageBuilder: (_) => CharacterListPage(),
+ initialRouteName: 'characters',
),
BottomNavigationTab(
bottomNavigationBarItem: BottomNavigationBarItem(
title: const Text('Quotes'),
icon: Icon(Icons.format_quote),
),
navigatorKey: GlobalKey<NavigatorState>(),
- initialPageBuilder: (_) => QuoteListPage(),
+ initialRouteName: 'quotes',
)
];@override
Widget build(BuildContext context) => AdaptiveBottomNavigationScaffold(
navigationBarItems: _bottomNavigationTabs,
);
}

There are two more places left, both inside the material_bottom_navigation_scaffold.dart, as that’s where we're subclassing and instantiating the above BottomNavigationTab.

剩下两个地方,都在material_bottom_navigation_scaffold.dart ,因为这是我们对上面的BottomNavigationTab子类化和实例化的BottomNavigationTab

class _MaterialBottomNavigationTab extends BottomNavigationTab {
const _MaterialBottomNavigationTab({@required BottomNavigationBarItem bottomNavigationBarItem,@required GlobalKey<NavigatorState> navigatorKey,
- @required WidgetBuilder initialPageBuilder,
+ @required String initialRouteName,@required this.subtreeKey,
}) : assert(bottomNavigationBarItem != null),
assert(subtreeKey != null),
- assert(initialPageBuilder != null),
+ assert(initialRouteName != null),
assert(navigatorKey != null),
super(
bottomNavigationBarItem: bottomNavigationBarItem,
navigatorKey: navigatorKey,
- initialPageBuilder: initialPageBuilder,
+ initialRouteName: initialRouteName,
);
final GlobalKey subtreeKey;
}void _initMaterialNavigationBarItems() {
materialNavigationBarItems.addAll(
widget.navigationBarItems
.map(
(barItem) => _MaterialBottomNavigationTab(
bottomNavigationBarItem: barItem.bottomNavigationBarItem,
navigatorKey: barItem.navigatorKey,
subtreeKey: GlobalKey(),
- initialPageBuilder: barItem.initialPageBuilder,
+ initialRouteName: barItem.initialRouteName,
),
)
.toList(),
);
}

Finally, it’s time to ensure that our additional navigators delegates its route generation to Fluro, as well as using the newest initialRouteName of the BottomNavigationTab. I'm using different bottom navigation menus for Android and iOS, so I needed to make that change in two files:

最后,它的时间,以确保我们的额外航海家代表其路由一代FLURO,以及使用最新的initialRouteName中的BottomNavigationTab 。 我为Android和iOS使用了不同的底部导航菜单,因此需要在两个文件中进行更改:

  1. material_bottom_navigation_scaffold.dart

    material_bottom_navigation_scaffold.dart

Widget _buildPageFlow(
BuildContext context,
int tabIndex,
_MaterialBottomNavigationTab item,
) {
final isCurrentlySelected = tabIndex == widget.selectedIndex;
// We should build the tab content only if it was already built or
// if it is currently selected.
_shouldBuildTab[tabIndex] =
isCurrentlySelected || _shouldBuildTab[tabIndex];
final Widget view = FadeTransition(
opacity: _animationControllers[tabIndex].drive(
CurveTween(curve: Curves.fastOutSlowIn),
),
child: KeyedSubtree(
key: item.subtreeKey,
child: _shouldBuildTab[tabIndex]
? Navigator(
// The key enables us to access the Navigator's state inside the
// onWillPop callback and for emptying its stack when a tab is
// re-selected. That is why a GlobalKey is needed instead of
// a simpler ValueKey.
key: item.navigatorKey,
- // Since this isn't the purpose of this sample, we're not using
- // named routes. Because of that, the onGenerateRoute callback
- // will be called only for the initial route.
- onGenerateRoute: (settings) => MaterialPageRoute(
- settings: settings,
- builder: item.initialPageBuilder,
- ),
+ initialRoute: item.initialRouteName,
+ // RouteFactory is nothing but an alias of a function that takes
+ // in a RouteSettings and returns a Route<dynamic>, which is
+ // the type of the onGenerateRoute parameter.
+ // We registered one of these in our main.dart file.
+ onGenerateRoute: Router.appRouter
+ .matchRoute(context, settings.name, routeSettings: settings)
+ .route,
)
: Container(),
),
);
if (tabIndex == widget.selectedIndex) {
_animationControllers[tabIndex].forward();
return view;
} else {
_animationControllers[tabIndex].reverse();
if (_animationControllers[tabIndex].isAnimating) {
return IgnorePointer(child: view);
}
return Offstage(child: view);
}
}

2. cupertino_bottom_navigation_scaffold.dart

2. cupertino_bottom_navigation_scaffold.dart

Widget build(BuildContext context) => CupertinoTabScaffold(
// As we're managing the selected index outside, there's no need
// to make this Widget stateful. We just need pass the selectedIndex to
// the controller every time the widget is rebuilt.
controller: CupertinoTabController(initialIndex: selectedIndex),
tabBar: CupertinoTabBar(
items: navigationBarItems
.map(
(item) => item.bottomNavigationBarItem,
)
.toList(),
onTap: onItemSelected,
),
tabBuilder: (context, index) {
final barItem = navigationBarItems[index];
return CupertinoTabView(
navigatorKey: barItem.navigatorKey,
- onGenerateRoute: (settings) => CupertinoPageRoute(
- settings: settings,
- builder: barItem.initialPageBuilder,
- ),
+ onGenerateRoute: (settings) {
+ // The [Navigator] widget has a initialRoute parameter, which
+ // enables us to define which route it should push as the initial
+ // one. See [MaterialBottomNavigationScaffold] for more details.
+ //
+ // The problem is that in the Cupertino version, we're not
+ // instantiating the [Navigator] ourselves, instead we're
+ // delegating it to the CupertinoTabView, which doesn't provides
+ // us with a way to set the initialRoute name. The best
+ // alternative I could find is to "change" the route's name of
+ // our RouteSettings to our BottomNavigationTab's initialRouteName
+ // when the onGenerateRoute is being executed for the initial
+ // route.
+ var routeSettings = settings;
+ if (settings.name == '/') {
+ routeSettings =
+ settings.copyWith(name: barItem.initialRouteName);
+ );
+ }
+ return Router.appRouter
+ .matchRoute(
+ context,
+ routeSettings.name,
+ routeSettings: routeSettings,
+ )
+ .route;
+ },
);
},
);

4.导航 (4. Navigate)

Everything we’ve done by now was about being prepared to handle any incoming web-like named routes, but we’re still not using them. We need to stop using Navigator.push and start using Navigator.pushNamed. There are two places for changing that:

到目前为止,我们所做的一切都是为了准备处理任何传入的类似Web的命名路由,但我们仍未使用它们。 我们需要停止使用Navigator.push并开始使用Navigator.pushNamed 。 有两个地方可以更改:

  • When selecting a character on the list.

    在列表上选择一个字符时。

character_list_page.dart

character_list_page.dart

- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => CharacterDetailPage(
- id: character.id,
- ),
- ),
+ Navigator.of(context).pushNamed(
+ 'characters/${character.id}',
);
  • When selecting a quote’s author on the list.

    在列表上选择报价的作者时。

quote_list_page.dart

quote_list_page.dart

Navigator.of(
context,
rootNavigator: true,
- ).push(
- MaterialPageRoute(
- fullscreenDialog: true,
- builder: (context) => CharacterDetailPage(
- name: quote.authorName,
- ),
- ),
+ ).pushNamed(
+ 'quotes/authors?name=${quote.authorName}',
);

We’re all done!

我们都做完了!

边注 (Side note)

I once read someone arguing that with Fluro, we’re not able to pass complex objects directly from one page to another. Although we can find obscure ways to do that, I don’t advise on doing so.

我曾经读过有人争论说,使用Fluro,我们无法将复杂的对象直接从一页传递到另一页。 尽管我们可以找到晦涩的方法,但我不建议这样做。

I firmly believe that we shouldn’t be passing complex objects around. An alternative I prefer is giving the object an id and storing it in your data layer. That way, we can pass only its id between pages.

我坚信我们不应该传递复杂的对象。 我更喜欢的替代方法是为该对象提供一个id ,并将其存储在您的数据层中。 这样,我们只能在页面之间传递其id

奖金 (Bonus)

You may have noticed that the route names are duplicated across the code. This isn’t a recommended practice at all. It served the purpose of improving the article’s readability, but I don’t want you to do that anywhere else. For inspiration on how to extract it, check out the master branch.

您可能已经注意到,路由名称在代码中重复。 根本不建议这样做。 它的目的是提高文章的可读性,但我不希望您在其他任何地方这样做。 有关如何提取它的灵感,请查看master分支。

翻译自: https://medium.com/better-programming/spotless-routing-and-navigation-in-flutter-995c647b9258

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值