Android 开发这几年最明显的一个变化就是“能跑就行”的时代彻底过去了。早年间做一个页面Activity 里塞几百行代码、网络请求和 UI 更新混在一起、屏幕一转数据全丢大家也都这么过来了。但只要项目稍微上点规模、团队超过三个人、需求迭代超过两个版本这种写法就会变成灾难现场改一个按钮的逻辑要翻三个文件内存泄漏查一整天生命周期回调写得像迷宫。Jetpack 这套东西本质上就是官方把过去十年里无数团队踩坑总结出来的最佳实践固化成了一个个可以直接拿来用的组件库。它不是什么新框架也不是要推翻你现有的代码而是一套“让 Android 开发变得可维护、可测试、可预期”的工具集合。我接触 Jetpack 是从 Lifecycle 和 ViewModel 开始的那时候只是单纯想解决“屏幕旋转后数据丢失”这个烦人问题后来越用越深才发现这套组件之间是互相咬合的ViewModel 靠 Lifecycle 感知生命周期LiveData 靠 Lifecycle 自动解绑DataBinding 靠 ViewModel 提供数据Room 靠协程做异步Navigation 又把 Fragment 之间的跳转和参数传递统一管起来。单独看每个组件都不复杂难的是理解它们为什么这样设计、在什么场景下该用哪个、以及怎么组合起来才不别扭。这篇内容我就按自己实际项目里的使用顺序把 Jetpack 的核心组件拆开讲清楚从设计思路到落地代码再到那些文档里不会写的坑尽量一次说明白。适合已经写过 Android、但项目结构还比较乱、想系统梳理一遍的中级开发者也适合刚接触 Jetpack、被一堆组件名字搞晕的新手。1. 先想清楚Jetpack 到底在解决什么问题1.1 从一次“屏幕旋转”事故说起很多人学 Jetpack 的第一个动机特别朴素屏幕一转数据没了。我最早遇到这个问题是在做一个表单页面用户填了十几个字段结果手机一横过来Activity 重建所有输入清空用户当场就想卸载。当时的土办法是在onSaveInstanceState里把每个字段塞进 Bundle字段一多代码就臭得没法看而且 Bundle 有大小限制稍微复杂点的对象根本存不下。这个问题的本质是 Android 的组件生命周期和“数据”是绑死的。Activity 会因为配置变更、内存回收、返回栈操作等各种原因被销毁重建但用户的数据不应该跟着一起死。Jetpack 里的ViewModel就是专门干这个的它挂在一个比 Activity 更长的生命周期上配置变更时 Activity 重建但 ViewModel 存活数据自然就保住了。这个设计背后其实是一个很清晰的职责划分——UI 控制器只管展示和交互数据持有和业务逻辑交给 ViewModel。想明白这一层后面所有组件的作用就都好理解了。1.2 组件之间的“咬合关系”比单个组件更重要新手最容易犯的错是把 Jetpack 当成一堆独立的库今天用个 LiveData明天加个 Room结果拼在一起互相打架。实际上这套东西是有内在逻辑的我画不出图但可以用一句话概括Lifecycle 是地基ViewModel 是数据中枢LiveData/Flow 是数据通道Room 是持久层Navigation 是页面调度DataBinding/ViewBinding 是视图绑定。举个具体的咬合例子你在 ViewModel 里放一个 LiveDataFragment 里 observe 它。当 Fragment 的 view 被销毁比如切到后台被回收LiveData 会自动感知到 Lifecycle 进入 DESTROYED 状态并移除观察者不会像以前那样回调到一个已经挂掉的 UI 上导致崩溃。这个“自动”不是魔法是 Lifecycle 在背后做生命周期感知。所以你会发现单独用 LiveData 而不理解 Lifecycle等于只用了它一半的能力。这也是我建议的学习顺序先搞懂 Lifecycle 的观察者模式再往上叠 ViewModel 和 LiveData最后才是 Room、Navigation 这些。1.3 什么项目适合上 Jetpack什么项目别硬上这里得说句实在话不是所有项目都值得全套上 Jetpack。我见过一个只有三个页面的工具类 App硬套 Navigation DataBinding Room结果编译时间翻倍代码量比原来还多。判断标准其实很简单看你的项目有没有“状态管理”和“数据持久化”的真实需求。如果是一个纯展示、无网络、无本地存储的小 Demo用 ViewBinding 替代 findViewById 就够了ViewModel 都可以不上。但只要项目涉及网络请求、列表分页、表单草稿、多页面共享数据那 ViewModel LiveData/Flow 基本是刚需。至于 Room只要你有本地缓存需求就值得用它的编译期 SQL 校验能帮你挡掉大量运行时崩溃。Navigation 则适合页面超过五个、跳转逻辑开始混乱的项目。我的经验是按需引入一个组件一个组件地加每加一个都要能说清楚它解决了什么具体痛点而不是为了“技术栈完整”而堆砌。2. 核心组件逐个拆原理、选型与实操要点2.1 Lifecycle一切生命周期感知的起点Lifecycle 的核心价值是把“生命周期回调”从 Activity/Fragment 里抽出来变成一个可以被任意对象观察的状态机。以前你在 Activity 的onStart里注册监听、onStop里反注册稍不注意就漏了导致泄漏现在只要你的组件实现DefaultLifecycleObserver然后getLifecycle().addObserver(this)剩下的注册反注册全自动。实操上有个细节很多人不知道LifecycleObserver的回调执行时机和 Activity 的真实回调是有微妙差别的。比如onDestroy回调在配置变更导致的销毁时也会触发如果你在里面做资源释放得先判断isChangingConfigurations。另外用LifecycleOwner的时候要注意 Fragment 的 viewLifecycleOwner 和 fragment 本身的 lifecycleOwner 是两回事——在 Fragment 里 observe LiveData 一定要用viewLifecycleOwner否则 Fragment 被加入返回栈但 view 已销毁时观察者还活着回调到空 view 上直接崩。这个坑我踩过不止一次属于必考项。2.2 ViewModel数据存活的正确姿势ViewModel 的原理说穿了不复杂它由ViewModelStore持有而ViewModelStore挂在NonConfigurationInstance上配置变更时这个实例会被系统保留并传给新的 Activity所以 ViewModel 跟着活下来了。理解这个机制很重要因为它决定了 ViewModel 的边界——它只在配置变更时存活进程被杀了一样没。所以别把 ViewModel 当数据库用它只是内存里的临时数据持有者。选型上现在新建项目我基本不用AndroidViewModel了因为它持有 Application 引用测试时不好 mock。需要 Context 的场景比如读资源、拿数据库实例我倾向于在 ViewModel 构造时注入依赖配合ViewModelProvider.Factory或者干脆用 Hilt。这里有个实操要点ViewModel 的构造函数不要做耗时操作它是在主线程创建的你在里面读数据库或者发网络请求直接卡 UI。正确做法是在init块里启动协程或者用viewModelScope.launch异步加载。2.3 LiveData 与 Flow该用哪个怎么选这是被问得最多的问题。我的结论很直接新项目优先用 FlowLiveData 留给纯 UI 状态和需要生命周期自动感知的场景。LiveData 的优势是自带生命周期感知observe 时自动处理 STARTED/STOPPEDUI 不可见时不回调这点在省电和避免无效刷新上很实用。但它的缺点也明显不支持背压、操作符少、线程切换靠postValue和setValue区分、在协程里用起来别扭。Flow 则是纯 Kotlin 的响应式流操作符丰富map、filter、debounce、flatMapLatest 应有尽有配合repeatOnLifecycle也能做到生命周期感知。我现在的习惯是数据层和 ViewModel 内部用 Flow暴露给 UI 的用StateFlow只有在需要“UI 不可见时自动暂停”这种强生命周期语义时才用 LiveData。注意StateFlow和 LiveData 有个关键区别StateFlow 只要有订阅者就会收到当前值不管 UI 是否可见所以配合repeatOnLifecycle(Lifecycle.State.STARTED)使用是标配漏了这层会导致后台还在刷新 UI。2.4 Room编译期校验的本地数据库Room 最大的价值不是“简化 SQLite”而是编译期 SQL 校验。你写错一个字段名编译直接报错而不是等到运行时崩溃。这个特性在团队协作里价值巨大尤其是表结构频繁变更的时候。用 Room 有几个必须知道的点实体类的主键、索引要提前设计好PrimaryKey(autoGenerate true)和业务主键的选择会影响后续迁移数据库版本升级必须写Migration别图省事用fallbackToDestructiveMigration那等于用户升级一次数据全丢。查询返回类型上Room 支持返回FlowListT配合协程做响应式查询数据一变 UI 自动更新这个体验非常好。但要注意在主线程调用 Room 的同步方法会直接抛异常所有数据库操作必须放到 IO 调度器或者用 suspend 函数。我一般会在 DAO 里全部用 suspend 或者返回 Flow从源头上杜绝主线程阻塞。2.5 Navigation页面跳转的统一调度Navigation 解决的是 Fragment 之间跳转、参数传递、返回栈管理这三件事。以前用FragmentTransaction手动 add/replace返回栈行为全靠自己维护稍微复杂点就乱。Navigation 用一张导航图nav graph把页面关系声明出来跳转用navigate(R.id.action_a_to_b)参数用 Safe Args 插件生成类型安全的传递类编译期就能发现参数不匹配。实操上有个坑要提醒Navigation 的navigate调用时机必须在主线程且不能在当前 Fragment 还没 attach 的时候调。我遇到过在协程里直接 navigate 导致IllegalStateException的情况解决办法是用viewLifecycleOwner.lifecycleScope.launch并在里面切回主线程。另外底部导航栏配合 Navigation 时多个 tab 的返回栈要用setupWithNavController处理否则切换 tab 后返回键行为会很怪。3. 从零搭一个可复现的 Jetpack 骨架3.1 依赖配置与版本对齐动手之前先把依赖理清楚Jetpack 组件版本之间是有兼容性要求的乱配版本是新手最常见的翻车点。下面是我在一个模拟项目里用的依赖组合用 Kotlin DSL 写在build.gradle.kts里dependencies { val lifecycleVersion 2.7.0 val roomVersion 2.6.1 val navVersion 2.7.6 // Lifecycle implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion) implementation(androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion) implementation(androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion) // Room implementation(androidx.room:room-runtime:$roomVersion) implementation(androidx.room:room-ktx:$roomVersion) ksp(androidx.room:room-compiler:$roomVersion) // Navigation implementation(androidx.navigation:navigation-fragment-ktx:$navVersion) implementation(androidx.navigation:navigation-ui-ktx:$navVersion) // ViewBinding 无需额外依赖buildFeatures 里开启即可 }这里有几个选择要解释一下。Room 的注解处理器我用 KSP 而不是 KAPT因为 KSP 是 Kotlin 原生方案编译速度比 KAPT 快不少尤其是项目大了以后差距明显。lifecycle 的 ktx 版本必须带上因为viewModelScope、lifecycleScope这些扩展都在 ktx 包里不带的话协程作用域用不了。版本号这块我建议用版本目录version catalog统一管理别散落在各个 module 里否则升级时到处改。3.2 分层结构UI、ViewModel、Repository、DataSource骨架的核心是分层。我用的结构是四层UI 层Activity/Fragment只负责渲染和事件转发ViewModel 层持有 UI 状态并调用 RepositoryRepository 层做数据源调度网络还是本地DataSource 层是 Room DAO 和网络接口。这个分层不是教条但它的好处是每一层职责单一测试时能单独 mock。具体到代码UI 层通过by viewModels()拿到 ViewModelobserve 状态ViewModel 里用StateFlow暴露 UI 状态用viewModelScope启动协程调 RepositoryRepository 里判断缓存是否有效有效读 Room无效发网络请求再写回 Room。这个“单一数据源”模式Single Source of Truth是 Jetpack 官方推荐的好处是 UI 永远只从 Room 读数据网络结果先落库再触发 UI 更新数据一致性有保证。3.3 一个完整的列表页实现拿一个列表页举例把上面的分层串起来。先定义实体和 DAOEntity(tableName articles) data class ArticleEntity( PrimaryKey val id: Long, val title: String, val summary: String, val updatedAt: Long ) Dao interface ArticleDao { Query(SELECT * FROM articles ORDER BY updatedAt DESC) fun observeAll(): FlowListArticleEntity Insert(onConflict OnConflictStrategy.REPLACE) suspend fun upsertAll(items: ListArticleEntity) }注意observeAll返回的是Flow这样数据库一有变化UI 自动收到新数据不需要手动刷新。upsertAll用REPLACE策略重复 id 直接覆盖省去先查后插的麻烦。Repository 层负责调度class ArticleRepository( private val dao: ArticleDao, private val api: ArticleApi ) { fun observeArticles(): FlowListArticleEntity dao.observeAll() suspend fun refresh() { val remote api.fetchArticles() dao.upsertAll(remote.map { it.toEntity() }) } }ViewModel 层把状态和事件分开class ArticleViewModel( private val repository: ArticleRepository ) : ViewModel() { val articles: StateFlowListArticleEntity repository .observeArticles() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) private val _loading MutableStateFlow(false) val loading: StateFlowBoolean _loading.asStateFlow() fun refresh() { viewModelScope.launch { _loading.value true try { repository.refresh() } finally { _loading.value false } } } }这里stateIn的WhileSubscribed(5000)是个关键参数意思是“最后一个订阅者消失后 5 秒内还有订阅者回来就不重启上游流”。这个 5 秒的缓冲是为了应对屏幕旋转这种短暂无订阅者的场景避免数据流被反复重建。这个参数值不是随便定的太短起不到缓冲作用太长会浪费资源5 秒是官方示例里常用的经验值。UI 层收集状态class ArticleFragment : Fragment(R.layout.fragment_article) { private val viewModel: ArticleViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.articles.collect { list - adapter.submitList(list) } } } viewModel.refresh() } }repeatOnLifecycle(STARTED)这层包裹是必须的它保证 Fragment 进入后台时停止收集回到前台重新收集既省资源又避免后台更新 UI 导致的崩溃。4. 踩坑实录那些文档里不会写的问题4.1 常见崩溃与异常速查下面这张表是我在实际项目里整理的高频问题基本覆盖了 Jetpack 使用中 80% 的崩溃场景问题现象根本原因解决方式IllegalStateException: Cant access ViewModels from detached fragment在 Fragment attach 之前调用了viewModels()把 ViewModel 获取移到onViewCreated之后LiveData 回调到已销毁的 view 导致空指针用了 fragment 的 lifecycleOwner 而非 viewLifecycleOwnerobserve 时统一用viewLifecycleOwnerRoom 主线程查询抛异常在 UI 线程调用了非 suspend 的 DAO 方法DAO 全部改成 suspend 或返回 FlowNavigation 跳转IllegalStateException在非主线程或 Fragment 未 attach 时 navigate用lifecycleScope.launch并切主线程StateFlow 后台仍在刷新 UI没有用repeatOnLifecycle包裹 collect所有 collect 都套repeatOnLifecycle(STARTED)ViewModel 里持有 Activity 引用导致泄漏把 Context 传进了 ViewModel用 Application Context 或注入依赖这张表里最值得展开说的是 StateFlow 那条。很多人从 LiveData 迁到 Flow 时直接把observe换成collect结果发现 App 切到后台后日志还在刷、网络还在请求。原因就是collect是挂起函数只要协程活着就一直收不像 LiveData 会自动感知生命周期。repeatOnLifecycle就是补上这层感知的它会在生命周期低于 STARTED 时取消收集协程回到 STARTED 时重新启动。这个机制一定要理解透否则迁移过程会踩一堆坑。4.2 我踩过的三个真实坑第一个坑是ViewModel 的 Factory 写错导致每次都是新实例。早期我手写 Factory 时create方法里每次都new一个 Repository结果 ViewModel 里缓存的数据在配置变更后莫名其妙丢了。后来才明白Factory 的职责是“创建”但 ViewModel 的复用是ViewModelStore管的只要 key 一致就会复用问题出在我把 Repository 也每次重建导致数据源不一致。解决办法是把 Repository 做成单例或者用依赖注入框架统一管理。第二个坑是Room 的 Flow 查询在事务里不触发。我在一个批量更新的场景里把多个 DAO 操作包在Transaction里结果 Flow 只在事务结束后触发一次中间状态收不到。这其实是预期行为Room 的 Flow 是基于 InvalidationTracker 的事务提交后才通知。如果你的业务需要中间状态得拆成多次独立操作或者用BehaviorSubject之类的中间层手动推送。第三个坑是Navigation 的 Safe Args 和 Parcelable 参数。传复杂对象时Safe Args 生成的代码要求参数实现 Parcelable 或 Serializable我一开始传了个普通 data class编译直接报错。后来给 data class 加上Parcelize注解并实现Parcelable接口才解决。这里要注意Parcelize需要 kotlin-parcelize 插件而且嵌套对象也要一起实现 Parcelable否则运行时才崩。4.3 性能与内存的几个实操心得Jetpack 用不好也会带来性能问题我总结了几个心得。第一LiveData 的observe不要在主线程做重活虽然回调在主线程但你可以把耗时逻辑丢到协程里回调里只做 UI 更新。第二Room 的查询尽量加索引尤其是ORDER BY和WHERE用到的字段不加索引在数据量上万后查询会明显变慢加索引的代价是写入稍慢和存储稍大这个权衡要按业务来。第三ViewModel 里不要放 Bitmap 这类大对象ViewModel 存活时间比 Activity 长大对象一直占着内存容易 OOM图片该用 Glide/Coil 的缓存机制就用。还有一个容易被忽略的点viewModelScope在 ViewModel 被 clear 时会自动取消所以你在里面启动的协程不用担心泄漏。但如果你在 ViewModel 里手动创建了CoroutineScope而不用viewModelScope那就得自己管理取消否则 ViewModel 销毁后协程还在跑持有引用导致泄漏。这个细节我在 code review 里见过好几次属于典型的“知道但容易忘”。5. 组件组合的进阶玩法与边界5.1 DataBinding 还是 ViewBinding别纠结这个问题我被问过太多次。我的答案很明确绝大多数项目用 ViewBinding 就够了DataBinding 只在需要双向绑定或者大量表达式绑定时才考虑。ViewBinding 只做一件事——把布局里的 view 生成类型安全的引用替代 findViewById零运行时开销编译快。DataBinding 则支持在 XML 里写表达式、绑定数据、双向绑定功能强但代价是编译变慢、错误信息难懂、XML 里混逻辑难维护。我现在的做法是新项目默认 ViewBinding只有在表单类页面需要双向绑定比如输入框和 ViewModel 字段实时同步时才局部引入 DataBinding。而且即便是 DataBinding我也尽量把逻辑放在 ViewModel 里XML 里只做简单的绑定表达式避免把业务逻辑写进布局文件。这个取舍的核心是“可维护性优先于炫技”XML 里写复杂表达式当时爽三个月后自己都看不懂。5.2 Hilt 要不要上什么时候上Hilt 是 Jetpack 官方推荐的依赖注入方案基于 Dagger 但简化了很多。要不要上我的判断标准是看你的依赖关系复杂度。如果项目里只有两三个 Repository、依赖关系简单手动构造完全够用上 Hilt 反而增加学习成本和编译时间。但如果项目模块多、依赖层次深、需要作用域管理比如单例、ViewModel 作用域、Activity 作用域那 Hilt 的价值就体现出来了。上 Hilt 的实操要点HiltAndroidApp标在 Application 上AndroidEntryPoint标在 Activity/Fragment 上HiltViewModel标在 ViewModel 上然后用Inject constructor注入依赖。注意Hilt 的 ViewModel 必须配合HiltViewModel和by viewModels()使用不能手动 new。另外 Hilt 的编译期检查很严格模块划分不对会直接编译失败这其实是好事把运行时问题提前到了编译期。5.3 协程与 Jetpack 的配合边界协程和 Jetpack 是天生一对但配合时有几个边界要清楚。viewModelScope默认跑在主线程所以你在里面直接调 Room 的 suspend 方法Room 内部会切到自己的 IO 调度器不用你手动 withContext。但如果你调的是网络请求得确认网络库是否自己切了线程Retrofit 的 suspend 接口是自动切的但如果你用的是回调式 API 包成 suspend就得自己withContext(Dispatchers.IO)。另一个边界是异常处理。viewModelScope.launch里抛出的异常如果没有 try-catch会直接导致 App 崩溃因为默认的 CoroutineExceptionHandler 没有设置。我的习惯是在 ViewModel 的基类里统一设置异常处理器或者在每个 launch 里用 try-catch 包住把异常转成 UI 状态比如一个 error StateFlow让 UI 决定怎么展示。这个模式比让异常直接崩掉要友好得多也符合“错误也是状态”的设计思路。6. 写在最后的几句实在话Jetpack 这套东西我用了几年下来最大的感受是它不会让你的代码自动变好但它会逼着你把职责分清楚。以前你可以把所有逻辑塞进 Activity现在用了 ViewModel 和 Repository你不得不思考“这段代码该放哪一层”这个思考过程本身就是进步。我见过不少团队上了 Jetpack 但代码依然乱原因就是只学了 API 没学设计思想把 ViewModel 当成第二个 Activity 用那还不如不上。如果你正准备系统梳理自己的项目结构我的建议是从 ViewModel ViewBinding 开始先把 UI 和数据分离这件事做扎实跑通了再逐步引入 Room 做本地缓存、引入 Flow 做响应式、引入 Navigation 管跳转。每引入一个组件都问自己一句“它解决了什么具体问题”答不上来就先别加。技术栈的完整度从来不是目标可维护、可测试、能快速定位问题才是。这套东西的价值要等你真正在半夜被线上崩溃叫醒、然后发现因为分层清晰十分钟就定位到问题的时候才能体会得最深。