导航,或用户如何在不同的屏幕之间切换,是一个需要掌握的重要概念。好的导航可以使你的应用程序有条不紊,并帮助用户在你的应用程序中找到自己的方向,而不会感到沮丧。
在上一章中,当你为用户创建一个杂货店清单来管理要买的东西时,你对导航有了小小的体会。当用户点击一个物品时,会显示物品的详细信息。
你将学习以下内容。
在本章结束时,你将知道导航到不同屏幕所需的一切!
注意。如果你想直接跳到代码,请跳到开始部分。如果你想先学习理论知识,请继续阅读!
如果你有iOS背景,你可能对UINavigationController很熟悉。这个控制器定义了一个基于堆栈的方案来管理和导航视图控制器。
在Android中,你使用JetpackNavigation来管理各种片段。
在Flutter中,你使用Navigator小部件来管理你的屏幕或页面。你可以把屏幕或页面看作是路线。
注意。本章交替使用这些术语,因为它们的意思都是一样的。
一个栈是一个管理页面的数据结构。你插入的元素是后进先出(LIFO),只有堆栈顶部的元素对用户是可见的。
例如,当用户查看一个杂货店项目的列表时,点击一个项目将GroceryItemScreen推到堆栈的顶部。一旦用户完成了修改,你就把它从堆栈中抛出来。
下面是导航栈的顶层和侧层视图。
现在,是时候快速浏览一下Navigator1.0了。
在Flutter1.22发布之前,你只能通过发出直接的命令,如"现在显示这个"或"移除当前屏幕并返回到前一个",来实现屏幕之间的转换。Navigator1.0为你提供了一套简单的API,用于在屏幕之间进行导航。最常见的包括。
那么你如何在你的应用程序中添加一个导航器呢?
大多数Flutter应用程序以WidgetsApp作为根部件开始。
注意。到目前为止,你已经使用了MaterialApp,它扩展了WidgetsApp。
WidgetsApp包装了许多其他你的应用程序需要的普通widget。在这些包装好的部件中,有一个顶层的Navigator来管理你推送和弹出的页面。
为了向用户显示另一个屏幕,你需要在Navigator栈上推送一个Route。下面是该代码的一个例子。
下面是如何从堆栈中弹出一个路线的方法。
Navigator.pop(context);这似乎很容易。那么为什么不直接使用Navigator1.0呢?嗯,它有一些缺点。
必要性的API可能看起来很自然,很容易使用,但实际上,它很难管理和扩展。
首先是没有好的方法来管理你的页面,而不保留你推拉屏幕的心理地图。
想象一下,一个新的开发人员刚刚加入你的团队。他们会从哪里开始呢?他们肯定会感到困惑。
此外,Navigator1.0并没有向开发者公开路由栈。这使得它很难处理复杂的情况,比如在页面之间添加和删除一个屏幕。
例如,在Fooderlich中,你想只在用户还没有完成入职时显示入职界面。用Navigator1.0处理这个问题很复杂。
最后,在安卓设备上,当你有嵌套的导航器或将Flutter添加到你的主机安卓应用时,后退按钮可能无法在导航器1.0中工作。
它包括以下关键组件。
势在必行的API是非常基本的,迫使你把push()和pop()函数放在你的widget层次结构中--它耦合了你所有的widget!为了呈现另一个屏幕,你还必须在widget的层次结构上放置回调。
下面是它是如何工作的。
这就是了!而不是要建立一个每个屏幕如何呈现和解散的心理思维图,状态驱动哪些页面出现。
如果你有一个现有的项目,你不需要迁移或转换你现有的代码来使用新的API。
这里有一些提示,可以帮助你决定哪个对你更有用。
接下来,你将获得一些关于Navigator2.0的实践经验。
注意。本章将重点介绍Navigator2.0的实现。要了解更多关于Navigator1.0的信息,请查看。
在AndroidStudio中打开启动项目,运行flutterpubget,然后运行该应用程序。
注意。最好从启动项目开始,而不是继续上一章的项目,因为启动项目包含一些本章特有的变化。
你会看到,Fooderlich的应用程序只显示一个Splash屏幕。
在你深入研究导航之前,这个启动项目中有一些新文件可以帮助你。
在in.dart中,Fooderlich现在是一个StatefulWidget。它可以监听状态变化并相应地重建相应的部件。
Fooderlich现在支持用户对黑暗模式的设置。
在lib/screens/中有八个新变化。
稍后,你将使用这些来构建你的认证UI流程。
对lib/models/中的文件有一些改变。
tab_manager.dart已被删除。取而代之的是,你将在app_state_manager.dart中管理用户的标签选择,你很快就会构建它。
此外,有三个新的模型对象。
assets/sample_data/包含以下模拟数据。
assets/包含新的图片,你将用它们来建立新的入职指南。
在pubspec.yaml中,有两个新包。
smooth_page_indicator:^0.2.3webview_flutter:^2.0.7下面是它们的作用。
smooth_page_indicator:当你滚动页面时显示一个页面指示器。webview_flutter:提供一个WebView小部件,在iOS或Android平台上显示网页内容。
如果你打开android/app/build.gradle,你会发现minSdkVersion现在是19,如下所示。
android{defaultConfig{ ...minSdkVersion19...}}这是因为webview_flutter依赖于AndroidSDK19或更高版本,以实现混合组成。
现在你知道有什么变化了,你可以快速浏览一下本章中你要构建的UI流程。
下面是你向用户展示的前三个屏幕。
从入职界面,用户进入应用程序的首页。他们现在可以开始使用该应用程序了。
该应用程序向用户展示了三个标签,有这些选项。
接下来,用户可以点击添加按钮,或者,如果食品杂货清单不是空的,他们可以点击一个现有的项目。这将呈现出食品杂货项目屏幕,如下所示。
现在,用户如何查看他们的个人资料或注销?他们首先点选个人资料的头像,如下图所示。
在档案屏幕上,他们可以做以下事情。
下面是一个用户切换到黑暗模式,然后打开raywenderlich.com的例子。
这里是整个导航层次的鸟瞰图。
注意。在本章资料的assets文件夹中,有一个大型的图片版本。
你的应用程序完成后将会非常棒。现在,是时候添加一些代码了!
第一步是定义你的应用程序的状态,它如何变化以及当变化发生时通知哪些组件。
在models目录下,创建一个名为app_state_manager.dart的新文件并添加以下内容。
在同一个文件中,找到//TODO:AddinitializeApp并将其替换为以下内容。
voidinitializeApp(){//7Timer(constDuration(milliseconds:2000),(){//8_initialized=true;//9notifyListeners();});}下面是代码的工作原理。
接下来,找到//TODO:添加login并将其替换为以下内容。
voidlogin(Stringusername,Stringpassword){//10_loggedIn=true;//11notifyListeners();}这个函数接收了一个用户名和密码。下面是它的作用。
接下来,找到//TODO:AddcompleteOnboarding并将其替换为以下内容。
voidcompleteOnboarding(){_onboardingComplete=true;notifyListeners();}调用completeOnboarding()将通知所有听众,用户已经完成了入职指导。
找到//TODO:添加goToTab并替换为以下内容:
voidgoToTab(index){_selectedTab=index;notifyListeners();}goToTab设置_selectedTab的索引并通知所有听众.
找到//TODO:添加goToRecipes并将其替换为以下内容:
voidgoToRecipes(){_selectedTab=FooderlichTab.recipes;notifyListeners();}这是一个帮助函数,可以直接进入食谱标签。
找到//TODO:Addlogout并将其替换为以下内容。
voidlogout(){//12_loggedIn=false;_onboardingComplete=false;_initialized=false;_selectedTab=0;//13initializeApp()。//14notifyListeners()。}当用户注销时,上面的代码。
请注意,所有这些函数都遵循相同的模式:它们设置一些不公开的值,然后通知监听器。这就是你要实现的单向数据流架构的本质。
最后,打开lib/models/models.dart,添加以下内容。
export'app_state_manager.dart';这样,你就把新创建的AppStateManager添加到桶文件中。你现在有了一个定义明确的应用程序状态模型和一个通知监听器状态变化的机制。这是很大的进步。现在,你将在应用程序中使用它!
打开lib/main.dart,找到//TODO:CreateAppStateManager并将其替换为以下内容。
final_appStateManager=AppStateManager();这里,你初始化了AppStateManager。
接下来,找到//TODO:AddAppStateManagerChangeNotifierProvider并将其替换为以下内容。
ChangeNotifierProvider(create:(context)=>_appStateManager,)。这为AppStateManager创建了一个变化提供者,所以widget的后代可以访问或监听应用程序的状态。
这就是全部!注意到你是如何首先定义你的应用程序的状态的吗?任何开发者看了这个文件都可以知道用户是如何与Fooderlich应用互动的。
不要关闭in.dart,你很快会再次更新它。接下来,你要添加一个路由器。
路由器配置导航器显示的页面列表。它监听状态管理器,并根据状态变化,配置页面路由列表。
在lib/下,创建一个名为navigation的新目录。在该文件夹中,创建一个名为app_router.dart的新文件。添加以下代码。
现在你已经定义了你的路由器,你将让它处理路由请求。
找到//TOOD:添加_handlePopPage并将其替换为以下内容。
下面是它的工作原理。
现在,要使用这个回调助手,找到//TODO:AddonPopPage并替换为以下内容。
onPopPage:_handlePopPage,这样,每次有页面从堆栈中弹出时,都会调用它。
现在,你需要连接状态管理器。当状态改变时,路由器将用一组新的页面重新配置导航器。
找到//TODO:AddListeners并将其替换为以下内容。
appStateManager.addListener(notifyListeners);groceryManager.addListener(notifyListeners);profileManager.addListener(notifyListeners);下面是状态管理器的作用。
当你处置路由器时,你必须删除所有监听器。如果忘记这样做将会抛出一个异常。
找到//TODO:Disposelisteners并将其替换为以下内容。
@overridevoiddispose(){ appStateManager.removeListener(notifyListeners);groceryManager.removeListener(notifyListeners); profileManager.removeListener(notifyListeners);super.dispose();}恭喜你,你刚刚设置了你的路由器小部件。现在,是时候使用它了!保持app_router.dart开放,你很快就会用到它。
新创建的路由器需要知道谁是管理者,所以你现在要把它连接到状态、杂货店和档案管理器。
打开in.dart,找到//TODO:Importapp_router。用下面的内容代替它。
import'navigation/app_router.dart';接下来,找到//TODO:定义AppRouter,用以下内容代替。
@overridevoidinitState(){_appRouter=AppRouter(appStateManager:_appStateManager,groceryManager:_groceryManager,profileManager:_profileManager,);super.initState();}现在你已经在initState()中初始化了你的应用路由器,然后再使用它。保持in.dart打开。
下一步,找到//TODO:用Routerwidget替换。用以下代码替换现有的home:constSplashScreen(),一行。
home:Router( routerDelegate:_appRouter,//TODO:AddbackButtonDispatcher),你不需要再导入Splash屏幕了。继续并删除以下代码。
import'screen/splash_screen.dart';你的路由器现在都设置好了!现在是时候让它玩玩屏幕了。
所有的基础设施都到位了,现在是时候根据路由定义要显示的屏幕了。但首先,看看目前的情况。在iOS上构建并运行。你会注意到在运行标签中的一个异常。
更糟糕的是,模拟器可能会显示红色的死亡屏幕。
这是因为Navigator页面不能是空的。应用程序抛出了一个异常,因为它不能生成一个路线。接下来你会通过添加屏幕来解决这个问题。
你将从头开始,显示Splash屏幕。
打开lib/screens/splash_screen.dart,添加以下导入。
import'package:provider/provider.dart';import'.../models/models.dart';接下来,找到//TODO:SplashScreenMaterialPageHelper并将其替换为以下内容。
staticMaterialPagepage(){returnMaterialPage(name:FooderlichPages.splashPath,key:ValueKey(FooderlichPages.splashPath),child:constSplashScreen(),);}在这里,你定义了一个静态方法来创建一个MaterialPage,设置适当的唯一标识符并创建SplashScreen。
接下来找到//TODO:InitializeApp并将其替换为以下内容。
现在,你要添加应用程序启动时显示的Splash屏幕。
回到app_router.dart,找到//TODO:AddSplashScreen并替换为以下内容。
if(!appStateManager.isInitialized)SplashScreen.page(),这里,你检查应用程序是否被初始化。如果没有,你将显示Splash屏幕。
执行一次热重启,你会看到下面的屏幕闪过。
你仍然会看到一个错误,但不要担心,它很快就会消失。
恭喜你,你刚刚设置了你的第一条路线现在,准备其他路由就容易多了。让app_router.dart打开。
下一组代码的更新将遵循一个类似的模式。
打开lib/screens/login_screen.dart,添加以下导入。
import'package:provider/provider.dart'。import'.../models/models.dart';接下来,找到//TODO:LoginScreenMaterialPageHelper并将其替换为以下内容。
staticMaterialPagepage(){returnMaterialPage(name:FooderlichPages.loginPath,key:ValueKey(FooderlichPages.loginPath),child:constLoginScreen());}在这里,你定义了一个静态方法,创建一个MaterialPage,设置一个唯一的键,并创建LoginScreen。保持login_screen.dart打开。
切换回app_router.dart,找到//TODO:添加LoginScreen并替换为以下内容。
回到login_screen.dart,找到//TODO:Login->Navigatetohome,并将其替换为以下内容。
打开lib/screens/onboarding_screen.dart,添加以下导入。
import'package:provider/provider.dart';import'.../models/models.dart';接下来,找到//TODO:AddOnboardingScreenMaterialPageHelper并替换为以下内容。
staticMaterialPagepage(){returnMaterialPage(name:FooderlichPages.onboardingPath,key:ValueKey(FooderlichPages.onboardingPath),child:constOnboardingScreen(),);}在这里,你配置了一个MaterialPage,设置了onboarding页面的唯一键,并创建了Onboarding屏幕部件。
返回到app_router.dart,找到//TODO:添加OnboardingScreen并替换为以下内容。
恭喜你,这是一个很好的进展。现在,你要添加逻辑来处理入职界面中触发的变化。
当用户点击跳过按钮而不是通过入职指南时,你要显示通常的主屏幕。
在onboarding_screen.dart中,找到//TODO:Onboarding->Navigatetohome并将其替换为以下内容。
接下来,你要处理当用户在入职界面上点击***返回时发生的事情。
回到app_router.dart,找到TODO:HandleOnboardingandSplash,用下面的内容替换它。
应用程序将返回Splash屏幕以重新初始化,如下图所示。
当用户点击Skip时,应用程序将显示主屏幕。打开lib/screens/home.dart,添加以下导入。
import'package:provider/provider.dart';import'.../models/models.dart';接下来,找到//TODO:HomeMaterialPageHelper并将其替换为以下内容。
staticMaterialPagepage(intcurrentTab){returnMaterialPage(name:FooderlichPages.home,key:ValueKey(FooderlichPages.home),child:Home(currentTab:currentTab,),);}在这里,你已经创建了一个静态的MaterialPage帮助器,并在主屏幕上显示当前标签。保持home.dart打开。
回到app_router.dart,找到//TODO:AddHome并将其替换为以下内容。
if(appStateManager.isOnboardingComplete)Home.page(appStateManager.getSelectedTab),这告诉你的应用程序只有在用户完成入职时才显示主页。
最后,你可以看到onboarding的实际效果了!
你会注意到,你不能切换到不同的标签。那是因为你还没有设置好状态处理。接下来你会做这个。
打开home.dart,找到//TODO:WrapConsumerforAppStateManager并将其替换为以下内容。
接下来,向下滚动到小组件的结尾,在结尾的}之前,添加以下内容。
},);确保你打开了自动格式化,并保存文件重新格式化。
你刚刚把你的整个widget包裹在一个Consumer里面。Consumer将监听应用程序的状态变化,并相应地重建其内部部件。
接下来,找到//TODO:Updateuser'sselectedtab并将其替换为以下内容。
现在,你想增加的是,点选浏览食谱按钮会将用户带到食谱标签。
打开empty_grocery_screen.dart,添加以下导入。
import'package:provider/provider.dart';import'.../models/models.dart';接下来,找到//TODO:Updateuser'sselectedtab并替换为以下内容。
为了测试它,点击底部导航栏中的要买标签,然后点击浏览食谱按钮。注意,应用程序会转到食谱标签,如下所示。
接下来,你将连接杂货店项目屏幕。打开lib/screens/grocery_item_screen.dart。找到//TODO:GroceryItemScreenMaterialPageHelper并将其替换为以下内容。
staticMaterialPagepage({GroceryItemitem,intindex,Function(GroceryItem)onCreate,Function(GroceryItem,int)onUpdate}){returnMaterialPage(name:FooderlichPages.groceryItemDetails,key:ValueKey(FooderlichPages.groceryItemDetails),child:GroceryItemScreen(originalItem:item,index:index,onCreate:onCreate,onUpdate:onUpdate,),);}在这里,你创建了一个静态页面助手,将GroceryItemScreen包裹在一个MaterialPage中。GroceryItem屏幕需要。
接下来,你将实现杂货店项目的屏幕。有两种方法来显示它。
接下来你将启用这些功能。
打开lib/screens/grocery_screen.dart,找到//TODO:CreateNewItem。用下面的内容来代替它。
接下来,回到app_router.dart,找到//TODO:Createnewitem并替换为以下内容。
//1如果(groceryManager.isCreatingNewItem)//2GroceryItemScreen.page(onCreate:(item){//3groceryManager.addItem(item);},),下面是如何让你导航到一个新的杂货店项目。
在你的应用程序运行时,执行热重启。你现在就可以创建一个新的杂货项目,如下图所示。
打开grocery_list_screen.dart,找到//TODO:Tapongroceryitem并将其替换为以下内容。
manager.groceryItemTapped(index);这就触发了groceryItemTapped(),让监听器知道用户选择了一个杂货店项目。
现在,回到app_router.dart,找到//TODO:SelectGroceryItemScreen并替换为以下内容。
//1if(groceryManager.selectedIndex!=null)//2GroceryItemScreen.page(item:groceryManager.selectedGroceryItem,index:groceryManager.selectedIndex,onUpdate:(item,index){//3groceryManager.updateItem(item,index);},),下面是代码的工作原理。
现在,你能够点击一个食品杂货项目,编辑它,并保存它!
有时,用户开始添加一个食品杂货项目,然后改变主意。为了应对这种情况,打开app_router.dart,找到//TODO:Handlestatewhenuserclosesgroceryitemscreen,用下面的内容代替。
if(route.settings.name==FooderlichPages.groceryItemDetails){ groceryManager.groceryItemTapped(null);}这确保了当用户从杂货店项目屏幕上点击后退按钮时,适当的状态被重置。
热重新启动,然后再次测试该序列。
请注意,该应用程序现在可以按预期工作。
用户还不能导航到个人资料屏幕。在你解决这个问题之前,你需要处理状态变化。
打开home.dart,找到//TODO:home->profile并将其替换为以下内容。
现在用户可以进入"简介"屏幕了,他们需要能够再次关闭它。
打开lib/screens/profile_screen.dart,找到//TODO:CloseProfileScreen并替换为以下内容。
现在,找到//TODO:ProfileScreenMaterialPageHelper并将其替换为以下内容。
staticMaterialPagepage(Useruser){returnMaterialPage(name:FooderlichPages.profilePath,key:ValueKey(FooderlichPages.profilePath),child:ProfileScreen(user:user),);}在这里,你为Profile屏幕创建一个助手MaterialPage。它需要一个用户对象。
接下来,打开app_router.dart,找到//TODO:AddProfileScreen,用下面的内容替换它。
if(profileManager.didSelectUser)ProfileScreen.page(profileManager.getUser),这将检查配置文件管理器,看用户是否选择了他们的配置文件。如果是,它就会显示Profile屏幕。
执行热重载并点击用户的头像。现在它将显示个人资料屏幕。
打开app_router.dart,找到//TODO:Handlestatewhenuserclosesprofilescreen并将其改为以下内容。
if(route.settings.name==FooderlichPages.profilePath){profileManager.tapOnProfile(false);}这将检查你弹出的路由是否确实是"profilePath",然后告诉"profileManager"个人资料屏幕不再可见。
现在点击X按钮,简介屏幕就会消失。
在"简介"屏幕内,你可以做三件事。
接下来,你将处理WebView屏幕。
回到profile_screen.dart,找到//TODO:Openraywenderlich.comWebView并将其替换为以下内容。
现在,打开webview_screen.dart,导入以下内容。
import'../models/models.dart';接下来,找到//TODO:WebViewScreenMaterialPageHelper并将其替换为以下内容。
staticMaterialPagepage(){returnMaterialPage(name:FooderlichPages.raywenderlich,key:ValueKey(FooderlichPages.raywenderlich),child:constWebViewScreen(),);}在这里,你创建了一个静态的MaterialPage,包裹了一个WebView屏幕部件。
接下来,回到app_router.dart。找到//TODO:AddWebViewScreen并将其替换为以下内容。
if(profileManager.didTapOnRaywenderlich)WebViewScreen.page(),这将检查用户是否点击了进入raywenderlich.com网站的选项。如果是,它就会呈现WebView屏幕。
热重载并转到个人资料屏幕。现在,点击查看raywenderlich.com,你会看到它出现在网络视图中,如下所示。
关闭该视图呢?
还是在app_router.dart中,找到//TODO:HandlestatewhenuserclosesWebViewscreen,用下面的内容替换。
if(route.settings.name==FooderlichPages.raywenderlich){profileManager.tapOnRaywenderlich(false);}在这里,你检查路由设置的名称是否是raywenderlich,然后在profileManager上调用适当的方法。
接下来,你将进行注销功能的工作。
为了处理注销用户,到profile_screen.dart,找到//TODO:Logoutuser。用下面的内容替换它。
保存你的改变。现在,从个人资料屏幕上点击注销,你会发现它回到了Splash屏幕,如下图所示。
接下来,你将处理安卓系统的返回按钮。
如果你一直在iOS上运行该项目,在你现有的设备或模拟器中停止该应用。现在,在一个安卓设备或模拟器上构建并运行你的应用程序。做好以下工作。
你期望它回到前一页。相反,它退出了整个应用程序!
要解决这个问题,打开in.dart,找到//TODO:AddbackButtonDispatcher,用下面的内容代替。
backButtonDispatcher:RootBackButtonDispatcher(),在这里,你设置了路由器部件的BackButtonDispatcher,它监听平台弹出的路由通知。当用户点击Android系统的返回按钮时,会触发路由器委托的onPopPage回调。
热重新启动你的应用程序,并再次尝试同样的步骤。
呜呼,它的行为符合预期!恭喜你,你现在已经完成了整个UI导航流程。
你还学会了创建一个路由器部件,它封装并配置了导航器的所有页面路线。现在,你可以在一个单一的路由器对象中轻松地管理你的导航流
为了学习这个主题,这里有一些高层次的理论和演练的建议。
Navigator2.0可能有点难以理解和单独管理。下面的软件包围绕着Navigator2.0的API,使路由和导航更容易。
还有很多事情你可以用Navigator2.0来做。在下一章中,你会看到支持网络URL和深层链接的内容!