跳到正文Lab学后总结 · Jimmy Huang's BlogLab学后总结
本页目录· 105 条
这一页有部分内容暂时无法显示,其余内容完整。
1. 一句话总结这个 Lab
这个 Lab 要教会你:在 Jetpack Compose 里把多个页面组织成一个真正可切换的 Android App,并理解 NavController + NavHost + route 这一套导航机制,最后再用 ViewModel 实现页面之间的数据共享。
2. 这个 Lab 实际做了什么
从当前可见材料来看,这个 Lab 的主线不是单纯“写几个页面”,而是分两层逐步完成一个多页面 App。
2.1 第一阶段:做一个最小可运行的多页面导航 App
可见材料说明,前半段会先做一个最小版本,用来验证导航本身能不能跑通。
这个最小 App 里主要有三个页面:
这三个页面本身不复杂,重点不是页面内容有多漂亮,而是让你先理解:
一个 Compose App 里可以有多个 screen;
每个 screen 需要一个 route,也就是页面的“地址”;
用户点击导航按钮后,App 不是重新启动,而是在不同 screen 之间切换;
真正负责切换页面的是 NavController;
真正负责显示当前页面的是 NavHost;
页面和 route 的对应关系由 NavGraph 管理。
你可以把这一阶段理解成:先搭一个小型导航骨架,确认页面切换这件事能工作。
2.2 第二阶段:尝试两种不同的导航入口
Navigation Rail 和 Bottom Bar 只是“导航按钮长在哪里”的区别,不是两套完全不同的页面系统。
2.3 第三阶段:扩展成 Trip Diary App
可见材料还提到,后半段会把同样的导航思路扩展到一个更完整的 Trip Diary App。
Home:展示欢迎页;
Trip:收集表单数据;
Diary:显示已经提交的 trip 内容。
不只是“能切页面”,还要让一个页面输入的数据,可以被另一个页面看到。
用户进入 Trip 页面;
用户填写旅行相关信息;
用户提交;
数据进入 ViewModel;
用户进入 Diary 页面;
Diary 页面从 ViewModel 读取已经提交的 trip 内容并显示。
2.4 这个 Lab 大概完成的技术链路
在 Compose 项目中加入 navigation-compose;
创建多个 screen;
给每个 screen 定义 route;
创建 NavController;
用 NavHost 声明 route 和 screen 的对应关系;
用 Navigation Rail 或 Bottom Bar 作为用户点击入口;
点击入口时通过 NavController 切换页面;
在 Trip Diary App 中,用 ViewModel 保存 Trip 页面提交的数据;
Diary 页面读取 ViewModel 中的数据并显示。
具体完整代码、文件名、每一步截图、PDF 中的完整资源配置,当前材料没有完整显示,所以这里不编造。
3. 每个核心组件有什么用
下面按初学者视角,把这个 Lab 里最容易遇到的核心组件逐个拆开讲。
3.1 navigation-compose
它是什么?
navigation-compose 是 Jetpack Compose 用来做页面导航的库。
没有它,你当然也可以自己用 if/else 控制显示哪个页面,但那样会很乱,也不适合真实项目。
它在这个 Lab 里负责什么?
它提供了 Compose Navigation 相关能力,例如:
NavController
NavHost
route
页面切换
导航图结构
没有它会发生什么?
没有它,你的 App 很难用规范方式管理多个页面。
和其他组件有什么关系?
NavController、NavHost、route 这些概念,都是建立在它之上的。
3.2 Screen
它是什么?
HomeScreen
HistoryScreen
ProfileScreen
TripScreen
DiaryScreen
在 Compose 里,一个 screen 通常就是一个 @Composable 函数。
它在这个 Lab 里负责什么?
HomeScreen 显示首页内容;
TripScreen 显示表单;
DiaryScreen 显示已经提交的旅行记录。
没有它会发生什么?
导航系统可以切换 route,但没有实际页面可以显示。
和其他组件有什么关系?
当 route 是 "home" 时,显示 HomeScreen();
3.3 Route
它是什么?
Route 是每个页面在导航系统里的名字,可以理解为页面地址。
"home"
"history"
"profile"
"trip"
"diary"
它在这个 Lab 里负责什么?
用户点击底部导航栏的 Home 按钮,本质上就是告诉 NavController:
没有它会发生什么?
没有 route,导航系统不知道每个页面叫什么,也不知道应该切到哪里。
和其他组件有什么关系?
Route 是 NavController 和 NavHost 之间沟通的“地址语言”。
3.4 NavController
它是什么?
NavController 是导航系统的控制器,可以理解为“遥控器”。
它在这个 Lab 里负责什么?
去 Home;
去 History;
去 Profile;
去 Trip;
去 Diary;
返回上一个页面。
当用户点击 Bottom Navigation Bar 或 Navigation Rail 里的按钮时,最终通常都会调用 NavController 去切换页面。
没有它会发生什么?
没有 NavController,按钮就只是按钮。
和其他组件有什么关系?
导航入口调用它;
NavHost 监听它;
route 是它要跳转的目标地址。
用户点击按钮 → 按钮通知 NavController → NavController 改变当前 route → NavHost 显示对应 screen。
3.5 NavHost
它是什么?
NavHost 是页面显示的容器,可以理解为“舞台”。
它在这个 Lab 里负责什么?
它根据当前 route 决定显示哪个 screen。
当前 route 是 "home",它就显示 Home;
当前 route 是 "history",它就显示 History;
当前 route 是 "profile",它就显示 Profile。
没有它会发生什么?
没有 NavHost,导航系统不知道页面应该显示在哪里。
NavController 可以发出“去 home”的指令,但没人真正负责把 HomeScreen 放到屏幕上。
和其他组件有什么关系?
它使用 NavController 监听当前导航状态;
它内部定义 route 和 screen 的对应关系;
它承载多个 screen,但一次通常只显示当前 route 对应的那个。
3.6 NavGraph
它是什么?
NavGraph 是导航图,可以理解为 App 的“页面路线表”。
它在这个 Lab 里负责什么?
它描述 App 有哪些页面,以及每个 route 对应哪个 screen。
没有它会发生什么?
没有导航图,App 不知道有哪些合法页面,也不知道 route 和 screen 怎么对应。
和其他组件有什么关系?
在 Compose 里,很多时候你是在 NavHost 内部用 composable(route) { ... } 的方式声明这个导航图。
NavHost 里面写的那些 composable("xxx") { XxxScreen() },就是在定义导航图。
3.7 Expanded Modal Navigation Rail
它是什么?
Navigation Rail 是一种侧边导航栏。
Expanded Modal Navigation Rail 可以理解为:在侧边展开出来的导航菜单。
它在这个 Lab 里负责什么?
Home;
History;
Profile;
或其他页面。
没有它会发生什么?
没有它,App 仍然可以导航,只是少了一种侧边入口。
你可以换成 Bottom Bar、按钮、抽屉菜单等其他入口。
和其他组件有什么关系?
它只是一个 UI 入口,点击后仍然需要调用 NavController。
Rail 是“门口的按钮”,不是“真正负责换房间的人”。
3.8 Bottom Navigation Bar
它是什么?
Bottom Navigation Bar 是底部导航栏。
它在这个 Lab 里负责什么?
用户点击底部某个 tab 后,App 切换到对应页面。
没有它会发生什么?
但对于手机 App 来说,底部导航是很常见、很适合主页面切换的方式。
和其他组件有什么关系?
它和 Navigation Rail 一样,最终都要把用户点击转换成:
所以它和 Rail 的区别主要是 UI 位置和交互方式,不是底层导航原理不同。
3.9 ViewModel
它是什么?
ViewModel 是用来保存 UI 状态和业务数据的组件。
在这个 Lab 里,它和 Trip Diary App 的关系比较明显。
它在这个 Lab 里负责什么?
可见材料提到:用 ViewModel 把 Trip 里的数据带到 Diary。
TripScreen 负责输入数据;
ViewModel 负责保存数据;
DiaryScreen 负责读取并显示数据。
没有它会发生什么?
没有 ViewModel,你可能会把数据直接塞进页面参数,或者用全局变量乱传。
页面之间耦合严重;
数据容易丢;
屏幕旋转或页面重组时状态管理混乱;
项目一大就很难维护。
和其他组件有什么关系?
ViewModel 负责“页面之间共享什么数据”。
3.10 Drawable 资源文件
它是什么?
Drawable 是 Android 项目里存放图片、图标等资源的地方。
它在这个 Lab 里负责什么?
可见材料里特别提醒:如果 menu_open_24dp.png 没有放进 drawable,那么 R.drawable.menu_open_24dp 会报错。
没有它会发生什么?
如果代码里引用了这个图片资源,但是项目里没有这个文件,编译会失败。
和其他组件有什么关系?
Navigation Rail 可能需要一个菜单图标。
4. 用初学者类比解释
4.1 Screen = 商场里的不同房间
Home 是大厅;
History 是历史记录室;
Profile 是个人资料室;
Trip 是登记旅行信息的前台;
Diary 是展示旅行记录的展览室。
4.2 Route = 房间门牌号
"home" 是大厅门牌;
"profile" 是个人资料室门牌;
"trip" 是旅行登记处门牌;
"diary" 是旅行日记展览室门牌。
4.3 NavController = 商场里的导航遥控器
NavController 就像商场管理员手里的遥控器。
管理员根据你说的 route,控制 App 切换到对应房间。
4.4 NavHost = 商场中央舞台
你切到 Profile,它就展示 Profile;
4.5 NavGraph = 商场地图
哪些房间存在;
每个房间的门牌号是什么;
门牌号对应哪个房间。
如果一个房间没有登记在地图上,用户就无法导航过去。
4.6 Navigation Rail / Bottom Bar = 商场入口指示牌
Navigation Rail 和 Bottom Navigation Bar 就像商场里的指示牌。
Rail 是侧边的指示牌;
Bottom Bar 是底部的指示牌。
它们只是摆放位置不同,本质上都是让用户选择“我要去哪里”。
真正带用户去对应房间的,还是 NavController。
4.7 ViewModel = 前台登记本
在 Trip Diary App 里,ViewModel 就像前台登记本。
用户在 Trip 页面填写旅行信息,相当于在前台登记:
然后 Diary 页面就像展览室,会从这本登记本里读取内容并展示。
如果没有登记本,Trip 页面填完的内容就很难稳定地交给 Diary 页面。
5. 重点区分容易混淆的概念
5.1 NavController 和 NavHost 的区别
NavController
NavHost
当当前 route 变成 "profile" 后,NavHost 负责把 ProfileScreen 显示出来。
一句话区分
NavController 是遥控器,NavHost 是电视屏幕。
5.2 NavHost 和 NavGraph 的区别
NavHost
NavGraph
在 Compose 代码里,它们经常写在一起,所以容易混。
NavHost 是舞台,NavGraph 是贴在舞台后台的节目单。
5.3 route 和 screen 的区别
route
screen
一句话区分
如果 route 写错了,就像门牌号写错,导航系统找不到对应页面。
5.4 Navigation Rail 和 Bottom Bar 的区别
Navigation Rail
通常在屏幕侧边,适合大屏、横屏或需要侧边导航的场景。
Bottom Bar
它们的共同点
一句话区分
Rail 和 Bottom Bar 是两种不同位置的按钮区,不是两套不同的导航系统。
5.5 导航和数据传递的区别
导航解决什么问题?
数据传递解决什么问题?
Diary 页面要显示 Trip 页面填过的什么内容?
为什么不能混在一起?
如果你把数据传递全部塞进导航逻辑里,项目会越来越乱。
NavController 管页面切换;
ViewModel 管数据状态。
一句话区分
导航负责“去哪”,ViewModel 负责“带什么数据”。
5.6 ViewModel 和普通变量的区别
普通变量
ViewModel
它可以被多个 screen 使用,也更适合保存 Trip Diary 这种跨页面数据。
一句话区分
普通变量像临时便利贴,ViewModel 像正式登记本。
5.7 谁能访问谁?
用户能直接点击访问的
用户可以通过 Navigation Rail 或 Bottom Bar 访问注册过的页面,例如:
在 Trip Diary App 中,用户可以通过页面流程访问:
用户不能访问的
用户不能访问没有注册到 NavHost 的 route。
但如果 NavHost 里没有注册 "settings",这个 route 就不是合法页面。
页面之间应该怎么交互?
页面切换交给 NavController;
页面共享数据交给 ViewModel。
5.8 哪个是入口?哪个是页面?哪个是数据中心?
6. 这个 Lab 有什么实际用途
这个 Lab 的实际用途非常大,因为几乎所有真实 Android App 都不是单页面 App。
6.1 真实 App 都需要多页面结构
如果你不会 Compose Navigation,你只能做单页面 demo,很难做真正完整的 App。
6.2 Bottom Navigation 是移动端常见主导航
很多 App 的核心页面都放在底部导航栏中,例如:
Home;
Discover;
History;
Profile。
你之后做课程项目、Android 作业、个人项目时,基本都会遇到类似结构。
6.3 Navigation Rail 适合大屏和响应式设计
平板;
横屏模式;
桌面化 Android App;
大屏设备;
左侧菜单式布局。
这和现代 Android 开发中的 adaptive layout、responsive UI 有关系。
6.4 ViewModel 是真实项目里管理页面状态的关键
Trip Diary App 的例子虽然简单,但它体现了一个真实项目原则:
页面不要自己乱保存共享数据,应该把状态放到合适的位置统一管理。
表单状态;
用户信息;
当前选择项;
API 返回数据;
页面加载状态;
错误信息;
多页面共享数据。
6.5 对后端开发和 DevOps 的间接帮助
虽然这个 Lab 是 Android 前端 Lab,但它也能帮助你理解软件架构里的通用思想:
入口和处理逻辑要分开;
UI 展示和状态管理要分开;
路由系统要集中管理;
不同模块之间不要直接乱连;
数据流向要清楚。
一个系统里,不同组件应该各司其职,而不是所有东西乱写在一起。
7. 最后画一张生动形象的示意图
下面这张图用 Mermaid 表示,不是图片文件。
你可以把它复制到支持 Mermaid 的 Markdown 编辑器里渲染。
最后复习:这周你真正要带走什么?
Compose 多页面 App 不是靠一堆 if/else 硬切页面,而是用 Navigation 体系管理。
每个页面要有 route,route 就是页面在导航系统里的地址。
NavController 负责发出导航命令,NavHost 负责显示当前页面。
Navigation Rail 和 Bottom Bar 都只是用户入口,底层还是同一套 NavController + NavHost。
Trip 页面输入的数据要给 Diary 页面看,应该用 ViewModel 这种状态管理方式,而不是让两个页面乱连。
导航负责“去哪一页”,ViewModel 负责“页面之间共享什么数据”。
未注册的 route 不能正常导航;缺失 drawable 资源会导致代码引用报错。
材料未显示 / 不确定的部分
当前可读取的 Notion 内容没有完整显示以下信息,因此这里不编造:
完整 Kotlin 代码;
具体 Gradle 文件内容;
完整 package name;
具体项目目录结构;
完整截图步骤;
Navigation Rail / Bottom Bar 的完整实现代码;
Trip Diary App 的完整 UI 细节;
表单字段的完整列表;
是否使用 Room、DataStore、Repository 或外部 API;
是否有作业提交要求或截图要求。
如果你后续能导出 PDF、复制完整 Notion 文本,或者发截图,我可以继续基于完整材料补一版更精确的 Lab Week 6 完整学习笔记。
Unresolved reference: menu_open_24dp
用户点击入口 → NavController → NavHost → 对应 Screen
flowchart TB
Title["Lab Week 6:Jetpack Compose 多页面导航与 Trip Diary App"]
User["用户<br/>点击导航入口"]
subgraph Entry["导航入口层:用户从哪里点进去"]
Rail["Expanded Modal<br/>Navigation Rail<br/>侧边导航栏"]
Bottom["Bottom Navigation Bar<br/>底部导航栏"]
end
subgraph NavSystem["Compose Navigation 核心层:真正负责切页面"]
NC["NavController<br/>导航遥控器<br/>负责 navigate(route)"]
Host["NavHost<br/>页面舞台<br/>显示当前 route 对应页面"]
Graph["NavGraph / composable routes<br/>路线表<br/>登记合法页面"]
end
subgraph SimpleApp["第一阶段:最小导航 App"]
Home["Home Screen<br/>首页"]
History["History Screen<br/>历史页"]
Profile["Profile Screen<br/>个人资料页"]
end
subgraph TripDiary["第二阶段:Trip Diary App"]
TripHome["Home<br/>欢迎页"]
Trip["Trip Screen<br/>填写 trip 表单"]
VM["TripViewModel<br/>保存 trip 数据<br/>页面共享状态"]
Diary["Diary Screen<br/>显示已提交 trip"]
end
BadRoute["拼错或未注册 route<br/>例如 settings 没有登记"]
DirectCall["Screen 之间直接硬调用<br/>例如 Home 直接 new Profile"]
GlobalVar["用全局变量乱传 trip 数据"]
Remember["你要记住的核心区别:<br/>1. Rail / Bottom Bar 是入口,不是导航系统本身。<br/>2. NavController 负责去哪,NavHost 负责显示哪一页。<br/>3. route 是门牌号,Screen 是房间本身。<br/>4. ViewModel 负责共享数据,不要把数据传递和页面跳转混在一起。"]
Title --> User
User --> Rail
User --> Bottom
Rail --> NC
Bottom --> NC
NC --> Host
Host --> Graph
Graph --> Home
Graph --> History
Graph --> Profile
Graph --> TripHome
Graph --> Trip
Trip --> VM
VM --> Diary
BadRoute -. "❌ 不能导航:NavHost 没登记" .-> Host
DirectCall -. "❌ 不推荐:页面强耦合" .-> Profile
GlobalVar -. "❌ 不推荐:状态容易乱" .-> Diary
Diary --> Remember
Profile --> Remember
classDef title fill:#eef2ff,stroke:#4f46e5,stroke-width:2px,color:#111827;
classDef entry fill:#ecfdf5,stroke:#059669,stroke-width:2px,color:#064e3b;
classDef nav fill:#eff6ff,stroke:#2563eb,stroke-width:2px,color:#1e3a8a;
classDef screen fill:#f8fafc,stroke:#64748b,stroke-width:1.5px,color:#0f172a;
classDef state fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f;
classDef danger fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d;
classDef remember fill:#f5f3ff,stroke:#7c3aed,stroke-width:2px,color:#2e1065;
class Title title;
class Rail,Bottom entry;
class NC,Host,Graph nav;
class Home,History,Profile,TripHome,Trip,Diary screen;
class VM state;
class BadRoute,DirectCall,GlobalVar danger;
class Remember remember;
navController.navigate("某个route")
navController.navigate("profile")
navController.navigate("settings")