[Flutter学徒] 7 - 路由&导航

1,367 阅读32分钟

本文由 简悦SimpRead 转码,原文地址 www.raywenderlich.com

了解Navigator 2.0与Navigator 1.0的区别,以及如何使用它来添加导航声明.......。

导航,或用户如何在不同的屏幕之间切换,是一个需要掌握的重要概念。好的导航可以使你的应用程序有条不紊,并帮助用户在你的应用程序中找到自己的方向,而不会感到沮丧。

在上一章中,当你为用户创建一个杂货店清单来管理要买的东西时,你对导航有了小小的体会。当用户点击一个物品时,会显示物品的详细信息。

但这使用的是导航的命令式风格,被称为导航仪1.0。在这一章中,你将学习以声明式的方式在屏幕之间进行导航。

你将学习以下内容。

  • 快速浏览 "Navigator "1.0。
  • 概述Navigator2.0和如何使用它。
  • 如何通过使用提供器包来驱动导航的状态。
  • 如何处理Android系统的返回按钮。

在本章结束时,你将知道导航到不同屏幕所需的一切!

注意。如果你想直接跳到代码,请跳到开始部分。如果你想先学习理论知识,请继续阅读!

介绍导航仪

如果你有iOS背景,你可能对UINavigationController很熟悉。这个控制器定义了一个基于堆栈的方案来管理和导航视图控制器。

在Android中,你使用Jetpack Navigation来管理各种片段。

在Flutter中,你使用Navigator小部件来管理你的屏幕或页面。你可以把屏幕页面看作是路线

注意。本章交替使用这些术语,因为它们的意思都是一样的。

一个是一个管理页面的数据结构。你插入的元素是后进先出(LIFO),只有堆栈顶部的元素对用户是可见的。

例如,当用户查看一个杂货店项目的列表时,点击一个项目将GroceryItemScreen推到堆栈的顶部。一旦用户完成了修改,你就把它从堆栈中抛出来

下面是导航栈的顶层和侧层视图。

现在,是时候快速浏览一下Navigator 1.0了。

导航仪1.0概述

在Flutter 1.22发布之前,你只能通过发出直接的命令,如 "现在显示这个 "或 "移除当前屏幕并返回到前一个",来实现屏幕之间的转换。Navigator 1.0为你提供了一套简单的API,用于在屏幕之间进行导航。最常见的包括。

  • push():在堆栈中添加一个新的路线。
  • pop():从堆栈中删除一个路由。

那么你如何在你的应用程序中添加一个导航器呢?

大多数Flutter应用程序以WidgetsApp作为根部件开始。

注意。到目前为止,你已经使用了MaterialApp,它扩展了WidgetsApp

WidgetsApp包装了许多其他你的应用程序需要的普通widget。在这些包装好的部件中,有一个顶层的Navigator来管理你推送和弹出的页面。

推送和弹出路线

为了向用户显示另一个屏幕,你需要在Navigator栈上推送一个Route。下面是该代码的一个例子。

bool result = await Navigator.push<bool>(
  context,
  MaterialPageRoute<bool>(
    builder: (BuildContext context) => OnboardingScreen()
  ),
);


这里,MaterialPageRoute返回你的新屏幕部件的实例。Navigator在屏幕从堆栈中弹出时返回推送的结果。

下面是如何从堆栈中弹出一个路线的方法。

Navigator.pop(context);


这似乎很容易。那么为什么不直接使用Navigator 1.0呢?嗯,它有一些缺点。

Navigator 1.0的缺点

必要性的API可能看起来很自然,很容易使用,但实际上,它很难管理和扩展。

首先是没有好的方法来管理你的页面,而不保留你推拉屏幕的心理地图。

想象一下,一个新的开发人员刚刚加入你的团队。他们会从哪里开始呢?他们肯定会感到困惑。

此外,Navigator 1.0并没有向开发者公开路由栈。这使得它很难处理复杂的情况,比如在页面之间添加和删除一个屏幕。

例如,在Fooderlich中,你想只在用户还没有完成入职时显示入职界面。用Navigator 1.0处理这个问题很复杂。

另一个缺点是Navigator 1.0并不更新网页的URL路径。任何时候你去一个新的页面,你只看到基本的URL,像这样。www.localhost:8000/#/。此外,网络浏览器的前进和后退按钮可能无法像预期那样工作。

最后,在安卓设备上,当你有嵌套的导航器或将Flutter添加到你的主机安卓应用时,后退按钮可能无法在导航器1.0中工作。

如果有一个声明式的API可以解决大部分的这些痛点,那不是很好吗?这就是为什么导航器2.0诞生的原因!

导航器2.0概述

Flutter 1.22引入了Navigator 2.0,一个新的声明式API,允许你完全控制你的导航栈。它的目标是在解决Navigator 1.0的痛点的同时,感觉更像Flutter。它的主要目标包括。

  • 公开导航仪的页面堆栈。你现在可以管理你的页面了。更多的权力,更多的控制!
  • 向后兼容命令式的API。你可以在同一个应用程序中同时使用命令式和声明式风格。
  • 处理操作系统的事件。更好地处理事件,如Android系统的返回按钮。
  • 管理嵌套的导航器。让你可以控制哪个导航器有优先权。
  • 管理导航状态。让你解析路线并处理网络URL和深度链接。

下面是构成Navigator 2.0声明性API的新抽象。

它包括以下关键组件。

  • Page:一个描述路由配置的抽象类。
  • Router:处理配置导航仪显示的页面列表。
  • RouterDelegate:定义了路由器如何监听应用程序状态的变化以重建导航仪的配置。
  • RouteInformationProvider:向路由器提供 "路由信息"。
  • RouteInformationParser:将路线信息解析为用户定义的数据类型。
  • BackButtonDispatcher:向路由器报告对平台系统的返回按钮的按压。
  • TransitionDelegate:决定页面如何进入和离开屏幕的过渡。

注意。本章将主要关注Navigator和RouterDelegate的使用。在下一章中,你将深入了解其他组件。

导航和单向数据流

势在必行的API是非常基本的,迫使你把push()pop()函数放在你的widget层次结构中--它耦合了你所有的widget! 为了呈现另一个屏幕,你还必须在widget的层次结构上放置回调。

通过新的声明式API,你现在可以单向地管理你的导航状态。小部件是状态驱动的,如下图所示。

下面是它是如何工作的。

  1. 一个用户点击一个按钮。
  2. 按钮处理程序告诉应用程序的状态要更新。
  3. 路由器是状态的监听器,所以当状态改变时它会收到通知。
  4. 基于新的状态变化,路由器重新配置导航器的页面列表。
  5. 导航器检测列表中是否有新的页面,并处理转换以显示该页面。

这就是了! 而不是要建立一个每个屏幕如何呈现和解散的心理思维图,状态驱动哪些页面出现。

导航仪2.0总是比导航仪1.0好吗?

如果你有一个现有的项目,你不需要迁移或转换你现有的代码来使用新的API。

这里有一些提示,可以帮助你决定哪个对你更有用。

  • 对于大中型应用。考虑使用声明式API和路由器小部件。你可能需要管理大量的导航状态。
  • 适用于小型应用。对于快速原型设计或为演示创建一个小的应用程序,命令式API是合适的。有时,推送和弹出就是你所需要的!

接下来,你将获得一些关于Navigator 2.0的实践经验。

注意。本章将重点介绍Navigator 2.0的实现。要了解更多关于Navigator 1.0的信息,请查看。

开始使用

在Android Studio中打开启动项目,运行flutter pub get,然后运行该应用程序。

注意。最好从启动项目开始,而不是继续上一章的项目,因为启动项目包含一些本章特有的变化。

你会看到,Fooderlich的应用程序只显示一个Splash屏幕。

别担心,你很快就会连接所有的屏幕。你将建立一个简单的流程,其中有一个登录屏幕和一个入职小部件,然后再展示你到目前为止所建立的基于标签的现有应用。但首先,你要看一下项目文件的变化。

对项目文件的修改

在你深入研究导航之前,这个启动项目中有一些新文件可以帮助你。

in.dart中,Fooderlich现在是一个StatefulWidget。它可以监听状态变化并相应地重建相应的部件。

Fooderlich现在支持用户对黑暗模式的设置。

screens文件夹中的新内容

lib/screens/ 中有八个新变化。

  • splash_screen.dart: 配置了初始的Splash屏幕。
  • login_screen.dart: 允许用户登录。
  • onboarding_screen.dart: 引导用户通过一系列的步骤来了解更多关于这个应用程序。
  • profile_screen.dart: 允许用户检查他们的个人资料,更新设置和注销。
  • home.dart: 现在在右上方包括一个档案按钮,供用户查看他们的档案。
  • screens.dart: 一个桶状文件,将所有的屏幕分组为一个单一的导入。

稍后,你将使用这些来构建你的认证UI流程。

对models文件夹的改动

lib/models/ 中的文件有一些改变。

tab_manager.dart已被删除。取而代之的是,你将在app_state_manager.dart中管理用户的标签选择,你很快就会构建它。

此外,有三个新的模型对象。

  • fooderlich_pages.dart。描述了每个页面的唯一键的列表。
  • user.dart。描述了一个单一的用户。包括用户的角色、个人资料图片、全名和应用程序设置等信息。
  • profile_manager.dart: 管理用户的档案状态,例如,获取用户的信息,检查用户是否正在查看他们的档案,以及设置黑暗模式。

其他资产

assets/sample_data/ 包含以下模拟数据。

  • sample_explore_recipes.json, sample_friends_feed.jsonsample_recipes.json。这些都包括一个`id'字段,为每个显示的瓦片提供一个唯一的键。
  • ExploreRecipeSimpleRecipePost也包括一个额外的id字段。

assets/ 包含新的图片,你将用它们来建立新的入职指南。

新的包

pubspec.yaml 中,有两个新包。

smooth_page_indicator: ^0.2.3
webview_flutter: ^2.0.7


下面是它们的作用。

smooth_page_indicator: 当你滚动页面时显示一个页面指示器。 webview_flutter: 提供一个WebView小部件,在iOS或Android平台上显示网页内容。

Android SDK版本

如果你打开android/app/build.gradle,你会发现minSdkVersion现在是19,如下所示。

android {
    defaultConfig {
    	...
        minSdkVersion 19
        ...
    }
}


这是因为webview_flutter依赖于Android SDK 19或更高版本,以实现混合组成。

注意。更多信息请查看webview_flutter文档pub.dev/packages/we…

现在你知道有什么变化了,你可以快速浏览一下本章中你要构建的UI流程。

纵观UI流程

下面是你向用户展示的前三个屏幕。

  1. 当用户启动应用程序时,他们将看到的第一个屏幕是闪屏。这给了开发者初始化和配置应用程序的机会。
  2. 一旦初始化,用户就会浏览到登录屏幕。用户现在必须输入他们的用户名密码,然后点击登录
  3. 一旦用户登录,一个入职屏幕显示他们如何使用该应用程序。用户有两个选择:扫过指南,了解更多关于该应用程序的信息,或者跳过。

从入职界面,用户进入应用程序的首页。他们现在可以开始使用该应用程序了。

该应用程序向用户展示了三个标签,有这些选项。

  1. 探索。查看当天的菜谱,看看他们的朋友在做什么。
  2. 食谱。浏览他们想做的菜谱集。
  3. 要买。将原料或物品添加到他们的食品杂货清单中。

接下来,用户可以点击添加按钮,或者,如果食品杂货清单不是空的,他们可以点击一个现有的项目。这将呈现出食品杂货项目屏幕,如下所示。

现在,用户如何查看他们的个人资料或注销?他们首先点选个人资料的头像,如下图所示。

档案屏幕上,他们可以做以下事情。

  • 查看他们的个人资料,看看他们已经获得了多少积分。
  • 将应用程序的主题改为黑暗模式。
  • 访问 raywenderlich.com 网站。
  • 退出应用程序。

下面是一个用户切换到黑暗模式,然后打开 raywenderlich.com 的例子。

当你点击注销时,它会重新初始化应用程序并进入登录屏幕,如下图所示。

这里是整个导航层次的鸟瞰图。

注意。在本章资料的assets文件夹中,有一个大型的图片版本。

你的应用程序完成后将会非常棒。现在,是时候添加一些代码了!

管理你的应用程序状态

第一步是定义你的应用程序的状态,它如何变化以及当变化发生时通知哪些组件。

models目录下,创建一个名为app_state_manager.dart的新文件并添加以下内容。

import 'dart:async';
import 'package:flutter/material.dart';

// 1
class FooderlichTab {
  static const int explore = 0;
  static const int recipes = 1;
  static const int toBuy = 2;
}

class AppStateManager extends ChangeNotifier {
  // 2
  bool _initialized = false;
  // 3
  bool _loggedIn = false;
  // 4
  bool _onboardingComplete = false;
  // 5
  int _selectedTab = FooderlichTab.explore;

  // 6
  bool get isInitialized => _initialized;
  bool get isLoggedIn => _loggedIn;
  bool get isOnboardingComplete => _onboardingComplete;
  int get getSelectedTab => _selectedTab;

  // TODO: 添加初始化应用程序
  // TODO: 添加登录
  // TODO: 增加 completeOnboarding
  // TODO: 添加 goToTab
  // TODO: 添加goToRecipes
  // TODO: 添加注销
}


AppStateManager管理应用程序的导航状态。花点时间来理解你所添加的属性。

  1. 为用户点击的每个标签创建常量。
  2. _initialized检查应用程序是否被初始化。
  3. _loggedIn让你检查用户是否已经登录。
  4. onboardingComplete检查用户是否完成了入职流程。
  5. _selectedTab记录用户在哪个标签上。
  6. 这些是每个属性的getter方法。你不能在AppStateManager之外改变这些属性。这对单向流架构很重要,在这种架构下,你不会直接改变状态,而只是通过函数调用或派发的事件。

现在,是时候学习如何修改应用程序的状态了。你将创建函数来改变上面所声明的每个属性。

初始化应用

在同一个文件中,找到// TODO: Add initializeApp并将其替换为以下内容。

void initializeApp() {
  // 7
  Timer(const Duration(milliseconds: 2000), () {
    // 8
    _initialized = true;
    // 9
    notifyListeners();
  });
}


下面是代码的工作原理。

  1. 在执行闭合之前设置一个2000毫秒的延迟定时器。这设置了用户启动应用程序后,应用程序屏幕将显示多长时间。
  2. 设置initializedtrue
  3. 通知所有听众。

登录

接下来,找到// TODO: 添加login并将其替换为以下内容。

void login(String username, String password) {
  // 10
  _loggedIn = true;
  // 11
  notifyListeners();
}


这个函数接收了一个用户名和密码。下面是它的作用。

  1. 设置loggedIntrue
  2. 通知所有听众。

注意。在一个真实的场景中,你会发出一个API请求来登录。然而,在这种情况下,你只是在使用一个模拟。

完成入职培训

接下来,找到// TODO: Add completeOnboarding并将其替换为以下内容。

void completeOnboarding() {
  _onboardingComplete = true;
  notifyListeners();
}


调用completeOnboarding()将通知所有听众,用户已经完成了入职指导。

设置所选标签

找到 // TODO: 添加 goToTab 并替换为以下内容:

void goToTab(index) {
  _selectedTab = index;
  notifyListeners();
}


goToTab设置_selectedTab的索引并通知所有听众.

导航到食谱标签

找到 // TODO: 添加 goToRecipes 并将其替换为以下内容:

void goToRecipes() {
  _selectedTab = FooderlichTab.recipes;
  notifyListeners();
}


这是一个帮助函数,可以直接进入食谱标签。

添加注销功能

找到 // TODO: Add logout 并将其替换为以下内容。

void logout() {
  // 12
  _loggedIn = false;
  _onboardingComplete = false;
  _initialized = false;
  _selectedTab = 0;

  // 13
  initializeApp()。
  // 14
  notifyListeners()。
}


当用户注销时,上面的代码。

  1. 重置所有应用程序的状态属性。
  2. 重新初始化应用程序。
  3. 通知所有监听器的状态变化。

请注意,所有这些函数都遵循相同的模式:它们设置一些不公开的值,然后通知监听器。这就是你要实现的单向数据流架构的本质。

最后,打开lib/models/models.dart,添加以下内容。

export 'app_state_manager.dart';


这样,你就把新创建的AppStateManager添加到桶文件中。你现在有了一个定义明确的应用程序状态模型和一个通知监听器状态变化的机制。这是很大的进步。现在,你将在应用程序中使用它!

使用新的AppStateManager

打开lib/main.dart,找到// TODO: Create AppStateManager并将其替换为以下内容。

final _appStateManager = AppStateManager();


这里,你初始化了AppStateManager

接下来,找到// TODO: Add AppStateManager ChangeNotifierProvider并将其替换为以下内容。

ChangeNotifierProvider(create: (context) => _appStateManager,)。


这为AppStateManager创建了一个变化提供者,所以widget的后代可以访问或监听应用程序的状态。

这就是全部! 注意到你是如何首先定义你的应用程序的状态的吗?任何开发者看了这个文件都可以知道用户是如何与Fooderlich应用互动的。

不要关闭in.dart,你很快会再次更新它。接下来,你要添加一个路由器。

创建路由器

路由器配置导航器显示的页面列表。它监听状态管理器,并根据状态变化,配置页面路由列表。

lib/下,创建一个名为navigation的新目录。在该文件夹中,创建一个名为app_router.dart的新文件。添加以下代码。

import 'package:flutter/material.dart';
import '../models/models.dart';
import '../screens/screens.dart';

// 1
class AppRouter extends RouterDelegate
    with ChangeNotifier, PopNavigatorRouterDelegateMixin {
  // 2
  @override
  final GlobalKey<NavigatorState> navigatorKey;

  // 3
  final AppStateManager appStateManager;
  // 4
  final GroceryManager groceryManager;
  // 5
  final ProfileManager profileManager;

  AppRouter({
    this.appStateManager,
    this.groceryManager,
		this.profileManager
  })
      : navigatorKey = GlobalKey<NavigatorState>() {
    // TODO: Add Listeners
  }

  // TODO: Dispose listeners

  // 6
  @override
  Widget build(BuildContext context) {
    // 7
    return Navigator(
      // 8
      key: navigatorKey,
      // TODO: Add onPopPage
      // 9
      pages: [
        // TODO: Add SplashScreen
        // TODO: Add LoginScreen
        // TODO: Add OnboardingScreen
        // TODO: Add Home
        // TODO: Create new item
        // TODO: Select GroceryItemScreen
        // TODO: Add Profile Screen
        // TODO: Add WebView Screen
      ],
    );
  }

  // TODO: Add _handlePopPage

  // 10
  @override
  Future<void> setNewRoutePath(configuration) async => null;
}


下面是路由器小部件的工作原理。

  1. 它扩展了RouterDelegate。系统将告诉路由器建立和配置一个导航器小部件。
  2. 声明GlobalKey,这是整个应用程序中唯一的密钥。
  3. 声明AppStateManager。路由器将监听应用程序的状态变化以配置导航器的页面列表。
  4. 声明GroceryManager,当你创建或编辑一个项目时监听用户的状态。
  5. 声明ProfileManager来监听用户配置文件的状态。
  6. RouterDelegate要求你添加一个build()。这将配置你的导航器和页面。
  7. 配置一个`导航器'。
  8. 使用navigatorKey,这是检索当前导航器所需要的。
  9. 声明pages,描述你的导航堆栈的页面堆栈。
  10. setNewRoutePath设置为null,因为你还不支持Flutter网络应用。暂时不用担心这个问题,你会在下一章学到更多关于这个话题的内容。

注意。这怎么是声明式的呢?你不是告诉导航器用push()pop()做什么,而是告诉它:当状态为x时,渲染y页。

现在你已经定义了你的路由器,你将让它处理路由请求。

处理弹出事件

找到 // TOOD: 添加 _handlePopPage 并将其替换为以下内容。

bool _handlePopPage(
  // 1
  Route<dynamic> route,
  // 2
  result) {
  // 3
  if (!route.didPop(result)) {
    // 4
    return false;
  }

  // 5
  // TODO: Handle Onboarding and splash
  // TODO: Handle state when user closes grocery item screen
  // TODO: Handle state when user closes profile screen
  // TODO: Handle state when user closes WebView screen
	// 6
  return true;
}


当用户点击返回按钮或触发系统的返回按钮事件时,会触发一个辅助方法,onPopPage

下面是它的工作原理。

  1. 这是当前的Route,它包含RouteSettings等信息,以检索路由的名称和参数。
  2. result是路由完成时返回的值--例如,一个对话框返回的值。
  3. 检查当前路由的弹出是否成功。
  4. 如果失败,返回false
  5. 如果路由弹出成功,这将检查不同的路由并触发适当的状态变化。

现在,要使用这个回调助手,找到// TODO: Add onPopPage并替换为以下内容。

onPopPage: _handlePopPage,


这样,每次有页面从堆栈中弹出时,都会调用它。

添加状态监听器

现在,你需要连接状态管理器。当状态改变时,路由器将用一组新的页面重新配置导航器。

找到// TODO: Add Listeners并将其替换为以下内容。

appStateManager.addListener(notifyListeners);
groceryManager.addListener(notifyListeners);
profileManager.addListener(notifyListeners);


下面是状态管理器的作用。

  • appStateManager: 确定应用程序的状态。它管理应用程序是否初始化了登录,以及用户是否完成了入职培训。
  • groceryManager: 管理食品杂货项目的列表和项目选择状态。
  • profileManager: 管理用户的档案和设置。

当你处置路由器时,你必须删除所有监听器。如果忘记这样做将会抛出一个异常。

找到 // TODO: Dispose listeners 并将其替换为以下内容。

@override
void dispose() {
	appStateManager.removeListener(notifyListeners);
  groceryManager.removeListener(notifyListeners);
	profileManager.removeListener(notifyListeners);
  super.dispose();
}


恭喜你,你刚刚设置了你的路由器小部件。现在,是时候使用它了! 保持app_router.dart开放,你很快就会用到它。

使用你的应用程序路由器

新创建的路由器需要知道谁是管理者,所以你现在要把它连接到状态、杂货店和档案管理器。

打开in.dart,找到// TODO: Import app_router。用下面的内容代替它。

import 'navigation/app_router.dart';


接下来,找到// TODO: 定义AppRouter,用以下内容代替。

AppRouter _appRouter;


一旦你声明了你的应用路由器,找到// TODO: Initialize app router并将其替换为以下内容。

@override
void initState() {
  _appRouter = AppRouter(
    appStateManager: _appStateManager,
    groceryManager: _groceryManager,
    profileManager: _profileManager,
  );
  super.initState();
}


现在你已经在initState()中初始化了你的应用路由器,然后再使用它。保持in.dart打开。

下一步,找到// TODO: 用Router widget替换。用以下代码替换现有的home: const SplashScreen(),一行。

home: Router(
	routerDelegate: _appRouter,
  // TODO: Add backButtonDispatcher
),


你不需要再导入Splash屏幕了。继续并删除以下代码。

import 'screen/splash_screen.dart';


你的路由器现在都设置好了! 现在是时候让它玩玩屏幕了。

添加屏幕

所有的基础设施都到位了,现在是时候根据路由定义要显示的屏幕了。但首先,看看目前的情况。在iOS上构建并运行。你会注意到在运行标签中的一个异常。

更糟糕的是,模拟器可能会显示红色的死亡屏幕。

这是因为Navigator页面不能是空的。应用程序抛出了一个异常,因为它不能生成一个路线。接下来你会通过添加屏幕来解决这个问题。

显示Splash屏幕

你将从头开始,显示Splash屏幕。

打开lib/screens/splash_screen.dart,添加以下导入。

import 'package:provider/provider.dart';
import '.../models/models.dart';


接下来,找到// TODO: SplashScreen MaterialPage Helper并将其替换为以下内容。

static MaterialPage page() {
  return MaterialPage(
    name: FooderlichPages.splashPath,
      key: ValueKey(FooderlichPages.splashPath),
      child: const SplashScreen(),);
}


在这里,你定义了一个静态方法来创建一个MaterialPage,设置适当的唯一标识符并创建SplashScreen

接下来找到// TODO: Initialize App并将其替换为以下内容。

Provider.of<AppStateManager>(context, listen: false).initializeApp()。


在这里,你使用当前的上下文来检索AppStateManager来初始化应用程序。

现在,你要添加应用程序启动时显示的Splash屏幕。

回到app_router.dart,找到// TODO: Add SplashScreen并替换为以下内容。

if (!appStateManager.isInitialized) SplashScreen.page(),


这里,你检查应用程序是否被初始化。如果没有,你将显示Splash屏幕。

执行一次热重启,你会看到下面的屏幕闪过。

你仍然会看到一个错误,但不要担心,它很快就会消失。

恭喜你,你刚刚设置了你的第一条路线 现在,准备其他路由就容易多了。让app_router.dart打开。

下一组代码的更新将遵循一个类似的模式。

  • 更新屏幕代码以通过管理器触发状态变化。
  • 更新路由器代码以处理新的状态变化,根据设置为当前的路由。

显示登录屏幕

你现在要实现路由逻辑的第一步:如果用户没有登录,在闪屏之后显示登录屏幕。

打开lib/screens/login_screen.dart,添加以下导入。

import 'package:provider/provider.dart'import '.../models/models.dart';


接下来,找到// TODO: LoginScreen MaterialPage Helper并将其替换为以下内容。

static MaterialPage page() {
  return MaterialPage(
      name: FooderlichPages.loginPath,
      key: ValueKey(FooderlichPages.loginPath),
      child: const LoginScreen());
}


在这里,你定义了一个静态方法,创建一个MaterialPage,设置一个唯一的键,并创建LoginScreen。保持login_screen.dart打开。

切换回app_router.dart,找到// TODO: 添加LoginScreen并替换为以下内容。

if (appStateManager.isInitialized && !appStateManager.isLoggedIn)
LoginScreen.page(),


这段代码说,如果应用程序初始化了,而用户还没有登录,它应该显示登录页面。

触发一个热重启。你会看到几秒钟的Splash屏幕,然后是登录屏幕。

恭喜你,错误已经消失了,你已经成功实现了路由。最后一步是处理登录状态的变化。

回到login_screen.dart,找到// TODO: Login -> Navigate to home,并将其替换为以下内容。

Provider.of<AppStateManager>(context, listen: false)
  .login('mockUsername', 'mockPassword');


这使用AppStateManager来调用一个函数来更新用户的登录状态。当登录状态改变时会发生什么?很高兴你问了,这是下一步。 :]

从登录界面过渡到入职界面

当用户登录后,你要显示入职界面。

打开lib/screens/onboarding_screen.dart,添加以下导入。

import 'package:provider/provider.dart';
import '.../models/models.dart';


接下来,找到// TODO: Add OnboardingScreen MaterialPage Helper并替换为以下内容。

static MaterialPage page() {
  return MaterialPage(
      name: FooderlichPages.onboardingPath,
      key: ValueKey(FooderlichPages.onboardingPath),
      child: const OnboardingScreen(),);
}


在这里,你配置了一个MaterialPage,设置了onboarding页面的唯一键,并创建了Onboarding屏幕部件。

返回到app_router.dart,找到// TODO: 添加OnboardingScreen并替换为以下内容。

if (appStateManager.isLoggedIn &&)
    !appStateManager.isOnboardingComplete)
OnboardingScreen.page(),


在这里,如果用户已经登录但还没有完成入职指南,你将显示入职界面。

再执行一次热重启,然后点击登录按钮。你会看到入职界面出现。

恭喜你,这是一个很好的进展。现在,你要添加逻辑来处理入职界面中触发的变化。

处理入职培训中的跳过和返回按钮

当用户点击跳过按钮而不是通过入职指南时,你要显示通常的主屏幕。

onboarding_screen.dart中,找到// TODO: Onboarding -> Navigate to home并将其替换为以下内容。

Provider.of<AppStateManager>(context, listen: false)
  .completeOnboarding();


在这里,点击Skip会触发completeOnboarding(),更新状态并表明用户完成入职。它还没有工作,所以如果你看到一个错误,不要惊慌。

接下来,你要处理当用户在入职界面上点击***返回时发生的事情。

回到app_router.dart,找到TODO: Handle Onboarding and Splash,用下面的内容替换它。

if (route.settings.name == FooderlichPages.onboardingPath) {
  appStateManager.logout();
}


如果用户从Onboarding屏幕上点击Back按钮,它会调用logout()。这将重置整个应用程序的状态,用户必须重新登录。

应用程序将返回Splash屏幕以重新初始化,如下图所示。

从入职到首页的过渡

当用户点击Skip时,应用程序将显示主屏幕。打开lib/screens/home.dart,添加以下导入。

import 'package:provider/provider.dart';
import '.../models/models.dart';


接下来,找到// TODO: Home MaterialPage Helper并将其替换为以下内容。

static MaterialPage page(int currentTab) {
  return MaterialPage(
      name: FooderlichPages.home,
      key: ValueKey(FooderlichPages.home),
      child: Home(
        currentTab: currentTab,
      ),);
}


在这里,你已经创建了一个静态的MaterialPage帮助器,并在主屏幕上显示当前标签。保持home.dart打开。

回到app_router.dart,找到// TODO: Add Home并将其替换为以下内容。

if (appStateManager.isOnboardingComplete)
Home.page(appStateManager.getSelectedTab),


这告诉你的应用程序只有在用户完成入职时才显示主页。

最后,你可以看到onboarding的实际效果了!

热重启,通过点击登录按钮导航到入职界面,然后点击跳转按钮。现在你会看到主屏幕。恭喜你!

你会注意到,你不能切换到不同的标签。那是因为你还没有设置好状态处理。接下来你会做这个。

处理标签的选择

打开home.dart,找到// TODO: Wrap Consumer for AppStateManager并将其替换为以下内容。

return Consumer<AppStateManager>(
  builder: (context, appStateManager, child) {


暂时不要理会任何红色的方块。

接下来,向下滚动到小组件的结尾,在结尾的}之前,添加以下内容。

  },);


确保你打开了自动格式化,并保存文件重新格式化。

你刚刚把你的整个widget包裹在一个Consumer里面。Consumer将监听应用程序的状态变化,并相应地重建其内部部件。

接下来,找到// TODO: Update user's selected tab并将其替换为以下内容。

Provider.of<AppStateManager>(context, listen: false)
  	.goToTab(index);


这里,你指定点选一个标签会调用goToTab()

处理浏览食谱按钮

现在,你想增加的是,点选浏览食谱按钮会将用户带到食谱标签。

打开empty_grocery_screen.dart,添加以下导入。

import 'package:provider/provider.dart';
import '.../models/models.dart';


接下来,找到// TODO: Update user's selected tab并替换为以下内容。

Provider.of<AppStateManager>(context, listen: false)
  .goToRecipes();


在这里,你指定点选浏览食谱调用goToRecipes()。这与你为标签所做的类似。

为了测试它,点击底部导航栏中的要买标签,然后点击浏览食谱按钮。注意,应用程序会转到食谱标签,如下所示。

显示杂货店项目屏幕

接下来,你将连接杂货店项目屏幕。打开lib/screens/grocery_item_screen.dart。找到// TODO: GroceryItemScreen MaterialPage Helper并将其替换为以下内容。

static MaterialPage page(
    {GroceryItem item,
    int index,
    Function(GroceryItem) onCreate,
    Function(GroceryItem, int) onUpdate}) {
  return MaterialPage(
      name: FooderlichPages.groceryItemDetails,
      key: ValueKey(FooderlichPages.groceryItemDetails),
      child: GroceryItemScreen(
        originalItem: item,
        index: index,
        onCreate: onCreate,
        onUpdate: onUpdate,
      ),);
}


在这里,你创建了一个静态页面助手,将GroceryItemScreen包裹在一个MaterialPage中。Grocery Item屏幕需要。

  1. 原始的杂货店项目,如果有的话。否则,它假定用户正在创建一个新的食品杂货项目。
  2. 2.选定的食品杂货项目的索引。
  3. onCreate当用户完成创建新项目时。
  4. onUpdate当用户完成更新一个项目时。

接下来,你将实现杂货店项目的屏幕。有两种方法来显示它。

  1. 用户点击 + 按钮来创建一个新的杂货项目。
  2. 2.用户点击一个现有的食品杂货项目来编辑它。

接下来你将启用这些功能。

创建一个新的食品杂货项目

打开lib/screens/grocery_screen.dart,找到// TODO: Create New Item。用下面的内容来代替它。

Provider.of<GroceryManager>(context, listen: false).createNewItem()。


这里,当用户点击**+**按钮时,你会触发对createNewItem()的调用。

接下来,回到app_router.dart,找到// TODO: Create new item并替换为以下内容。

// 1
如果(groceryManager.isCreatingNewItem)
// 2
GroceryItemScreen.page(
  onCreate: (item) {
    // 3
    groceryManager.addItem(item);
  },),


下面是如何让你导航到一个新的杂货店项目。

  1. 检查用户是否正在创建一个新的杂货店项目。
  2. 如果是,显示杂货店项目屏幕。
  3. 一旦用户保存了该项目,就更新食品杂货清单。

在你的应用程序运行时,执行热重启。你现在就可以创建一个新的杂货项目,如下图所示。

编辑一个现有的食品杂货项目

打开grocery_list_screen.dart,找到// TODO: Tap on grocery item并将其替换为以下内容。

manager.groceryItemTapped(index);


这就触发了groceryItemTapped(),让监听器知道用户选择了一个杂货店项目。

现在,回到app_router.dart,找到// TODO: Select GroceryItemScreen并替换为以下内容。

// 1
if (groceryManager.selectedIndex != null)
// 2
GroceryItemScreen.page(
  item: groceryManager.selectedGroceryItem,
  index: groceryManager.selectedIndex,
  onUpdate: (item, index) {
    // 3
    groceryManager.updateItem(item, index);
  },),


下面是代码的工作原理。

  1. 检查是否有一个杂货店项目被选中。
  2. 如果是,则创建杂货店项目屏幕页面。
  3. 当用户改变并保存一个项目时,它就会在当前的索引中更新该项目。

现在,你能够点击一个食品杂货项目,编辑它,并保存它!

撤销杂货店项目屏幕

有时,用户开始添加一个食品杂货项目,然后改变主意。为了应对这种情况,打开app_router.dart,找到// TODO: Handle state when user closes grocery item screen,用下面的内容代替。

if (route.settings.name == FooderlichPages.groceryItemDetails) {
	groceryManager.groceryItemTapped(null);
}


这确保了当用户从杂货店项目屏幕上点击后退按钮时,适当的状态被重置。

热重新启动,然后再次测试该序列。

  1. 点击 + 按钮,创建一个新的杂货店项目。
  2. 点击 < 按钮,返回。

请注意,该应用程序现在可以按预期工作。

导航到个人资料屏幕

用户还不能导航到个人资料屏幕。在你解决这个问题之前,你需要处理状态变化。

打开home.dart,找到// TODO: home -> profile并将其替换为以下内容。

Provider.of<ProfileManager>(context, listen: false)
.tapOnProfile(true);


这样,每当用户点击简介按钮,就会触发tapOnProfile()

现在用户可以进入 "简介 "屏幕了,他们需要能够再次关闭它。

打开lib/screens/profile_screen.dart,找到// TODO: Close Profile Screen并替换为以下内容。

Provider.of<ProfileManager>(context, listen: false)
  .tapOnProfile(false);


这将处理用户点击X(关闭)按钮时发生的动作。它更新了配置文件的状态,所以导航器会删除配置文件屏幕。

现在,找到// TODO: ProfileScreen MaterialPage Helper并将其替换为以下内容。

static MaterialPage page(User user) {
  return MaterialPage(
      name: FooderlichPages.profilePath,
      key: ValueKey(FooderlichPages.profilePath),
      child: ProfileScreen(user: user),);
}


在这里,你为Profile屏幕创建一个助手MaterialPage。它需要一个用户对象。

接下来,打开app_router.dart,找到// TODO: Add Profile Screen,用下面的内容替换它。

if (profileManager.didSelectUser)
ProfileScreen.page(profileManager.getUser),


这将检查配置文件管理器,看用户是否选择了他们的配置文件。如果是,它就会显示Profile屏幕。

执行热重载并点击用户的头像。现在它将显示个人资料屏幕。

打开app_router.dart,找到// TODO: Handle state when user closes profile screen并将其改为以下内容。

if (route.settings.name == FooderlichPages.profilePath) {
  profileManager.tapOnProfile(false);
}


这将检查你弹出的路由是否确实是 "profilePath",然后告诉 "profileManager "个人资料屏幕不再可见。

现在点击X按钮,简介屏幕就会消失。

导航到 raywenderlich.com

在 "简介 "屏幕内,你可以做三件事。

  1. 改变黑暗模式的设置。
  2. 访问 raywenderlich.com。
  3. 登出。

接下来,你将处理WebView屏幕。

从简介过渡到WebView

回到profile_screen.dart,找到// TODO: Open raywenderlich.com WebView并将其替换为以下内容。

Provider.of<ProfileManager>(context, listen: false)
  .tapOnRaywenderlich(true);


这里,你是说当用户点击相应的按钮时调用tapOnRaywenderlich()。这将触发你的路由器部件的重建,并添加WebView屏幕。

现在,打开webview_screen.dart,导入以下内容。

import '../models/models.dart';


接下来,找到// TODO: WebViewScreen MaterialPage Helper并将其替换为以下内容。

static MaterialPage page() {
  return MaterialPage(
      name: FooderlichPages.raywenderlich,
      key: ValueKey(FooderlichPages.raywenderlich),
      child: const WebViewScreen(),);
}


在这里,你创建了一个静态的MaterialPage,包裹了一个WebView屏幕部件。

接下来,回到app_router.dart。找到// TODO: Add WebView Screen并将其替换为以下内容。

if (profileManager.didTapOnRaywenderlich)
WebViewScreen.page(),


这将检查用户是否点击了进入 raywenderlich.com 网站的选项。如果是,它就会呈现WebView屏幕。

热重载并转到个人资料屏幕。现在,点击查看 raywenderlich.com,你会看到它出现在网络视图中,如下所示。

关闭该视图呢?

还是在app_router.dart中,找到// TODO: Handle state when user closes WebView screen,用下面的内容替换。

if (route.settings.name == FooderlichPages.raywenderlich) {
  profileManager.tapOnRaywenderlich(false);
}


在这里,你检查路由设置的名称是否是raywenderlich,然后在profileManager上调用适当的方法。

接下来,你将进行注销功能的工作。

注销

为了处理注销用户,到profile_screen.dart,找到// TODO: Logout user。用下面的内容替换它。

// 1
Provider.of<ProfileManager>(context, listen: false)
    .tapOnProfile(false);
// 2
Provider.of<AppStateManager>(context, listen: false).logout();


下面是注销动作触发时的情况。

  1. 将用户配置文件的tap状态设置为false
  2. 调用logout(),重设整个应用程序的状态。

保存你的改变。现在,从个人资料屏幕上点击注销,你会发现它回到了Splash屏幕,如下图所示。

接下来,你将处理安卓系统的返回按钮。

处理安卓系统的返回按钮

如果你一直在iOS上运行该项目,在你现有的设备或模拟器中停止该应用。现在,在一个安卓设备或模拟器上构建并运行你的应用程序。做好以下工作。

  1. 通过应用程序导航到要购买标签。
  2. 2.点击 + 按钮。
  3. 点击安卓系统的返回按钮,而不是应用的返回按钮。

你期望它回到前一页。相反,它退出了整个应用程序!

要解决这个问题,打开in.dart,找到// TODO: Add backButtonDispatcher,用下面的内容代替。

backButtonDispatcher: RootBackButtonDispatcher(),


在这里,你设置了路由器部件的BackButtonDispatcher,它监听平台弹出的路由通知。当用户点击Android系统的返回按钮时,会触发路由器委托的onPopPage回调。

热重新启动你的应用程序,并再次尝试同样的步骤。

呜呼,它的行为符合预期! 恭喜你,你现在已经完成了整个UI导航流程。

关键点

  • 你可以把另一个路由器包裹在一个包裹性的小部件中。
  • Navigator 1.0对于快速和简单的原型,呈现警报和对话框很有用。
  • Navigator 2.0在管理导航堆栈时需要更多的控制和组织时很有用。
  • 在Navigator 2.0中,导航器部件持有一个MaterialPage对象的列表。
  • 使用一个路由器部件来监听导航状态的变化并配置你的导航器的页面列表。
  • 设置路由器的返回按钮调度器让你监听平台系统事件。

何去何从?

你现在已经学会了如何以声明的方式在屏幕之间进行导航。你没有在不同的部件中调用push()pop(),而是使用多个状态管理器来管理你的状态。

你还学会了创建一个路由器部件,它封装并配置了导航器的所有页面路线。现在,你可以在一个单一的路由器对象中轻松地管理你的导航流

为了学习这个主题,这里有一些高层次的理论和演练的建议。

其他需要查看的库

Navigator 2.0可能有点难以理解和单独管理。下面的软件包围绕着Navigator 2.0的API,使路由和导航更容易。

还有很多事情你可以用Navigator 2.0来做。在下一章中,你会看到支持网络URL和深层链接的内容!


www.deepl.com 翻译