在Android开发里,页面层的数据同步问题曾经是让我最头疼的事。Activity一转屏,数据没了;接口回调回来,UI还没刷新;稍不留神,内存泄漏。后来我把项目的页面架构切换到Android Jetpack的ViewModel、LiveData和DataBinding这一套组合,配合生命周期管理,很多问题从根上被解决了。这篇文章把我在实际项目里的架构落地过程、踩坑记录和实践心得整理出来,适合准备重构页面架构的团队,也适合刚开始学Jetpack、想把LiveData和DataBinding这些组件真正用明白的开发者。
1. 为什么页面架构要换成ViewModel加LiveData加DataBinding
1.1 传统写法的三个问题
传统页面代码基本上就是Activity里面写逻辑。启动时初始化控件和加载数据,接口回调回来就findViewById、setText,离开页面时在onDestroy里取消注册、释放资源。这套写法在小页面还能扛住,一旦页面复杂,三个问题会接连冒出来。
第一个问题是配置变更导致的数据丢失。转屏的时候Activity重建,所有成员变量归零,如果你的网络请求是在旧Activity发的,回调回来之后Activity已经销毁,轻则空指针,重则内存泄漏。第二个问题是手动同步UI状态,状态一多就会漏。比如一个登录按钮,要同时考虑用户名密码是否非空、是否正在请求、是否通过校验,这些状态散落在多个回调里,维护起来非常容易出错。第三个问题是View操作和业务逻辑纠缠在一起,Activity越来越臃肿,想拆不好拆。
我前几年接手过一个提交表单页,转屏后整个表单清空,用户填了三分钟的东西没了,气得直接在应用商店给一星。后来加了InstanceState硬存,十几个字段每个都要手动存和恢复,每次加字段都要改三四个方法。这种体验让我下定决心,必须换一套能从设计上解决这些问题的架构。
1.2 三个组件各自的定位
Jetpack这套组合能解决上面的问题,靠的是职责分离。ViewModel负责数据持有:它把UI状态放进独立于Activity的对象里,配置变更后同一个实例还能拿回来,数据自然不丢。LiveData负责通知变化:它是一种能感知生命周期的数据容器,只在页面处于活跃状态时向观察者分发最新值,页面销毁后能自动清理观察者。DataBinding负责视图和数据绑定:把原本在代码里写的那套赋值、监听操作变成布局文件里的声明式表达式,让数据和UI的关系一目了然。
三个组件合在一起,数据在ViewModel里住着,变化通过LiveData广播,布局通过DataBinding自动订阅。Activity里只需要把三者组装起来,剩下交给框架。用我的话来说,以前是页面拉数据、找控件、写监听,现在是数据自己会跑到该去的控件上。
1.3 选型前要确认的环境事项
这套架构对项目环境有一定要求。Android Studio建议用较高版本,Gradle插件至少4.1以上,AndroidX依赖是必须的。targetSdk版本目前主流都到Android 12/13了,实测DataBinding在这些系统上表现正常。需要注意,Android 12开始系统对Activity启动、PendingIntent等有更严格限制,但这和Jetpack本身不冲突,不用过度担心。
另外,接入前最好先统一Kotlin版本和依赖库版本。如果项目里还有老support库,要全部迁移到AndroidX。迁移过程本身可能有点繁琐,但不迁移的话,DataBinding自动生成的类会出现找不到AndroidX依赖的一堆编译错误。我建议在项目结构允许的情况下,优先在新建的页面模块里实验这套架构,跑通后再铺开。
2. ViewModel:把页面数据装进保险箱
2.1 它怎么做到转屏不丢数据
ViewModel的生命周期范围不是跟着Activity实例走,而是跟着ViewModelStoreOwner走。Activity实现了ViewModelStoreOwner接口,系统在Activity配置变更销毁时,不会把ViewModelStore清掉,而是保存起来,等新Activity创建后重新取回。所以你在onCreate里拿到的ViewModel和转屏前是同一个对象,里面的成员变量天然保留。
简单记忆是:Activity的finish和Configuration Change是两回事。普通销毁并重建时,同一个ViewModel会被新Activity重新找到;真正finish时,ViewModelStore才会被清理。这个机制看着简单,其实是Android系统为了实现“场景恢复”做的关键设计。
2.2 onCleared的用途和注意点
ViewModel在真正销毁时会调用onCleared()。这个回调官方文档写得很简略,实际里非常有用。我习惯在ViewModel里保存协程的Job、RxJava的Disposable、网络请求的Cancelable,然后在onCleared里统一取消或释放。这样页面离开后,后台任务立即停止,不会把结果抛给已销毁的UI。
需要注意,onCleared被调用的时候,页面一般已经finish,也就是说Activity已经不在活跃状态。如果在onCleared里尝试更新UI,比如调用binding.tv.setText,很可能空指针或白白执行多余操作。正确姿势是,凡是UI相关操作都通过LiveData暴露出去,让UI层自己响应。
2.3 获取ViewModel的正确和错误方式
Kotlin项目里最推荐的方式是:
private val viewModel: UserViewModel by viewModels()这是activity-ktx提供的扩展,按懒加载方式创建,并绑定到当前Activity的ViewModelStore。如果是在Fragment里,写法一样,但依赖fragment-ktx。Java项目用:
UserViewModel viewModel = new ViewModelProvider(this).get(UserViewModel.class);它能拿到当前Activity对应的实例,配置变更后重新执行get时返回的还是同一个。
千万别自己new ViewModel。自己new出来的对象就是普通Java对象,不挂在ViewModelStore上,转屏必丢,也不会收到onCleared。我见过有同事在基类里写了一个createViewModel()返回new出来的实例,等于把Jetpack的救命机制又绕开了。
2.4 带参数构造的ViewModel
如果ViewModel构造函数需要参数,比如用户ID,就要通过Factory创建:
class UserViewModel(private val userId: String) : ViewModel() { class Factory(private val userId: String) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return UserViewModel(userId) as T } } }Activity里用:
val viewModel: UserViewModel by viewModels { UserViewModel.Factory(intent.getStringExtra("USER_ID") ?: "") }这里的关键是Factory必须返回正确的ViewModel类型,强转时要小心。如果你觉得工厂代码写起来啰嗦,官方也提供了viewModelFactory { initializer { ... } }这个DSL写法,但需要引入lifecycle-viewmodel-ktx等依赖。
2.5 不要在ViewModel里存Activity的Context
最后一个高频坑是存Context。ViewModel寿命比Activity长,假如你为了弹Toast或拿资源,把Activity引用放进了ViewModel,那么Activity销毁后依然被ViewModel握住,导致整个Activity以及它关联的所有View都泄漏。我见过最严重的案例,是进入页面十几次后直接OOM。
如果确实需要上下文,优先用AndroidViewModel,它接收的是Application这个单例,不会泄漏。或者在方法调用时把Context作为参数短暂传进去,用完就丢。总之一句话,ViewModel只管数据,别管UI和界面上下文。
3. LiveData:一种会看眼色的数据容器
3.1 LiveData怎么感知生命周期
LiveData的核心设计是持有数据和观察者,观察者注册时带着LifecycleOwner。LiveData内部根据owner的当前状态决定是否允许派发数据:只有状态是STARTED或RESUMED才会派发;如果owner处于DESTROYED,就会移除观察者,让整个通知链路自动断掉。这就是“会看眼色”的意思。
以前用EventBus和接口回调的时候,数据分发不关心页面状态。页面退到后台,通知照样来,代码里判断不得不塞一堆if。LiveData把这些判断从业务代码移到了框架里,页面不活跃就不打扰,活跃后立刻拿到最新值。这个特性在配合DataBinding时尤其省心。
3.2 setValue和postValue的区别
LiveData提供了两个公开方法更新值。setValue只能在主线程调用,调用后立即同步通知所有处于活跃状态的观察者;postValue可以在任意线程调用,内部会把更新动作post到主线程任务队列,由主线程来真正更新。
很多人搞不清什么时候用哪个,我的建议是:只要你在协程或回调里,默认用postValue;如果已经切回主线程,用setValue。postValue有一个容易被坑的语义:它只保留最后一个值,连续post多个值时,中间值可能会被合并丢弃。比如一个加载进度的例子,1秒内从10%快速推进到90%,UI最终可能只看到90%,看不到中间状态。
如果你的业务需要展示每个中间值,直接给LiveData传一个进度数据类,或者保证在主线程使用setValue,把这个时序问题交给场景设计去规避。
3.3 DataBinding为什么能省掉手动observe
如果在XML里写@{viewModel.userName},DataBinding会自动注册一个Observer到对应的LiveData上,注册所用LifecycleOwner来自Binding的lifecycleOwner属性。这样我们就不用再在Activity里写:
viewModel.userName.observe(this) { name -> binding.tvName.text = name }关键点在于,XML里绑定LiveData依赖binding.lifecycleOwner。有些人只设置了binding.viewModel,忘了设置lifecycleOwner,运行时总在奇怪为什么数据不自动刷新。设置完了,LiveData的所有生命周期行为才会接管数据更新。
我遇到过另一个问题:XML里绑了LiveData,代码里又手动observe了一遍,结果同一个数据变化导致两次赋值,虽然视觉上可能没区别,但回调日志会暴露问题。建议:能在XML里绑定的就用XML,需要复杂逻辑的UI再在代码里observe,并且XML里不要再绑定同一个字段。
3.4 一次性事件怎么用Event包装
LiveData的重放机制对恢复UI状态很方便,但也会带来一个反直觉行为:配置变更后,新的observer会立刻收到上一次的值。像Snackbar、Toast、导航这类动作,转屏后会重新执行一遍,用户会觉得莫名其妙。
处理方式一般是用Event包装类,里面的数据在被消费一次后就不再下发。我常用的实现:
class Event<out T>(private val content: T) { var hasBeenHandled = false private set fun getContentIfNotHandled(): T? { return if (hasBeenHandled) null else { hasBeenHandled = true content } } }ViewModel里用MutableLiveData<Event<String>>(),UI层observe后调用getContentIfNotHandled()。这段代码其实不是线程安全的,但在UI主线程回调场景下基本够用。如果想要更安全,可以用AtomicBoolean,或者直接上Kotlin Flow的SharedFlow。
4. DataBinding:把UI声明成数据和XML的关系
4.1 DataBinding和ViewBinding怎么选
ViewBinding是Google后来提供的轻量绑定方案,只生成View的引用,没有表达式功能;DataBinding不仅包含ViewBinding的功能,还支持在XML里写数据表达式、双向绑定、事件绑定、BindingAdapter。如果打算走“ViewModel + LiveData + DataBinding”这套架构,肯定要选DataBinding。
现在新项目的官方推荐是优先ViewBinding,但ViewBinding解决不了数据驱动渲染问题。我的建议是:只要页面数据来自ViewModel且需要自动刷新,就用DataBinding;如果只是静态布局,用ViewBinding甚至findViewById也不丢人。不要为了用而用。
4.2 在Android Studio中启用DataBinding
以现在的Android Studio来说,模块build.gradle的写法是:
android { buildFeatures { dataBinding true } }开启后,任何根标签为<layout>的XML都会在编译期生成对应的Binding类。比如activity_user.xml生成ActivityUserBinding。这个类里的字段就是XML中定义的那些控件,以及setViewModel之类的方法。
很多人在开启后遇到类找不到,大概率是没做干净构建。我一般是改完gradle后直接Rebuild Project,而不是只点Sync。另外,如果项目里同时开了ViewBinding和DataBinding,注意它们生成的类可能会重叠,建议确认清楚依赖在哪,避免命名冲突。
4.3 从@{ }到@{= }:单向绑定和双向绑定
DataBinding表达式的入口在布局根标签:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="viewModel" type="com.example.UserViewModel" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewModel.userName}" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={viewModel.userName}" /> </LinearLayout> </layout>@{viewModel.userName}表示数据变化会推给TextView,这是从ViewModel到View的单向数据流。@={viewModel.userName}多一个=号,表示双向绑定,既会在数据变化时更新EditText,也会在EditText输入时把字符串回写到ViewModel。这个多出来的等号我一开始老写漏,一漏就陷入“界面改了但数据源没变”的迷局。
表达式里还支持常见运算符。比如空合并:
android:text="@{viewModel.userName ?? '未登录'}"三目运算符:
android:visibility="@{viewModel.isVip ? View.VISIBLE : View.GONE}"但要注意,不要把复杂业务逻辑塞进XML。比如多次方法调用、操作集合,这些应该放到ViewModel或BindingAdapter里,XML只做声明。
4.4 设置lifecycleOwner是必须的一步
拿到Binding后,三行代码不能少:
binding = DataBindingUtil.setContentView(this, R.layout.activity_user) binding.lifecycleOwner = this binding.viewModel = viewModel第一行是绑定布局,生成Binding。第二行是设置LifecycleOwner,让LiveData表达式可以根据页面生命周期自动订阅和清理。第三行是传递ViewModel实例给布局变量。
为什么不设置lifecycleOwner就会出现问题?因为XML里括住的LiveData表达式本质上也要注册Observer,而不带LifecycleOwner的Observer是observeForever的模式,不受生命周期管控。页面销毁后,这个观察者还挂着,数据一变,旧View就会被更新,出现泄漏风险。跑几次内存分析就会看到一堆DataBinding的监听对象挂着Activity引用。
4.5 事件绑定和BindingAdapter
DataBinding里绑事件非常直接:
<Button android:onClick="@{() -> viewModel.onSave()}" />如果列表点击需要带参数,可以写@{(user) -> viewModel.onItemClick(user)}。
自定义属性用BindingAdapter。比如图片加载:
@BindingAdapter("app:imageUrl") fun loadImage(imageView: ImageView, url: String?) { if (url != null) { Glide.with(imageView.context).load(url).into(imageView) } }BindingAdapter是个静态函数,注意第一个参数是被绑定属性的那个View,第二个参数是XML里表达式的值。所有使用该自定义属性的XML都会调用这个方法。方法名随意,类型签名才是关键。用着用着会发现,这个机制非常适合提取一些跨页面通用的UI逻辑,比如为空显示默认图、时间格式化等。
5. 组合实战:从ViewModel到DataBinding的完整落地
5.1 构建一个简单的用户信息页
我用一个常见的用户页来串一遍完整流程。功能要求:显示当前用户名,用户点击保存后把用户名改成新值,保存过程中显示一个ProgressBar。页面结构非常小,但足够验证整套链路。
ViewModel写起来极其简单:
class UserViewModel : ViewModel() { val userName = MutableLiveData("老张") val isSaving = MutableLiveData(false) fun onSave() { isSaving.value = true userName.value = "已保存的新昵称" isSaving.value = false } }实际项目中可能会有网络请求、动态变化,但这几行已经体现了DataBinding参与的写法:viewModel直接暴露数据,UI不用关心谁修改了它。
5.2 布局里的绑定写法
布局文件的核心部分:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="viewModel" type="com.example.app.UserViewModel" /> <import type="android.view.View" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewModel.userName}" /> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.isSaving ? View.VISIBLE : View.GONE}" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="保存" android:onClick="@{() -> viewModel.onSave()}" /> </LinearLayout> </layout>这里<import>引入View类后,表达式里才能直接用View.VISIBLE。如果不加import,就要写成android.view.View.VISIBLE,太长了。onClick用了lambda表达式写法,DataBinding会把点击事件映射到viewModel.onSave方法。
5.3 Activity代码压缩到极致
有了DataBinding,Activity的onCreate只需要三行核心逻辑:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityUserBinding private val viewModel: UserViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_user) binding.lifecycleOwner = this binding.viewModel = viewModel } }不需要findViewById,不需要setText,不需要监听EditText。整个页面的初始数据和更新逻辑都转移到了ViewModel和XML里。如果之后要增加一个昵称输入框,只需在布局里加个EditText并双向绑定到userName,Activity完全不用改。
5.4 executePendingBindings什么时候用
DataBinding默认会在下一帧布局前执行绑定,这意味着在某些需要“立刻拿到绑定后控件状态”的场景,数据会滞后一个帧。比如在onCreate里设置数据后马上判断控件宽度,可能拿到旧值。这时可以调用:
binding.executePendingBindings()强制立即执行挂起的绑定任务。这个API我在列表页里用得比较多,因为RecyclerView的item绑定后需要立刻算高度,如果不执行,可能会出现闪烁或item高度不对。日常的普通页面,多数情况下不需要主动调。
5.5 转屏和前后台切换的实测
我实际测试的时候专门转了两次屏,发现TextView上的昵称在转屏前是“新的昵称”,转屏后依然是“新的昵称”。这个“依然”是LiveData的功劳:新Activity创建后,Binding里的LifecycleOwner把Observer注册上去,LiveData立刻回放当前值,所以UI没有任何延迟地恢复了。
切到后台再回来,因为页面可能进入过STOPPED状态,LiveData会在进入STARTED后重新把最新值发给UI。如果值没有变化,UI不会多次刷新。这一点比手写一套“保存状态恢复状态”要可靠得多。
6. 生命周期管理与数据绑定联动的关键细节
6.1 先有lifecycleOwner,才有生命周期感知
绑定布局后设置lifecycleOwner,这件事怎么强调都不过分。LiveData在DataBinding中自动订阅,用的是Binding内部注册的观察者,而这个观察者最终挂在谁身上,取决于lifecycleOwner属性。
如果不设置lifecycleOwner,DataBinding仍会在值变化时刷新UI,但那是通过Observable类的OnPropertyChangedCallback机制,没有生命周期拦截。结果是页面已经进入后台,数据一来UI照常更新,页面销毁后观察者还留在那里。所以我在给团队定的代码规范里就有一条:凡是在XML里绑LiveData的页面,必须在onCreate里设置lifecycleOwner。
6.2 配置变更时数据是怎么流动的
当转屏发生时,系统先暂停并销毁旧Activity,但ViewModelStore被保留。新Activity创建后,by viewModels()会从同一个Store里取出旧ViewModel,然后执行onCreate,生成新的Binding,设置lifecycleOwner和ViewModel。
这时候LiveData发现自己有了新的活跃观察者,就把最新值又派发了一次。于是新界面自动显示旧数据。这个链路里没有一个方法是程序员手写的,但每段都有框架兜底。理解了这个过程,你就明白为什么LiveData重放机制、ViewModel保留机制、DataBinding绑定都是缺一不可的。
6.3 内存泄漏的几个红线
我排查过很多次内存泄漏,和这套架构相关的常见泄漏有三个。
第一,ViewModel持有Activity或View,这是最常见也最严重的。第二,LiveData的observeForever忘了removeObserver,这通常在不需要生命周期参与的场景出现,但很多人直接用forever图省事,最后把自己套进去。第三,BindingAdapter里如果写成内部类而非顶层函数,并且访问了外部Activity字段,同样会持有Activity。
我的建议是把BindingAdapter都写成顶层函数,或者放在单独的文件里。ViewModel里只保留Application和可清理的资源。页面销毁流程做好后,用Memory Profiler反复进出页面几次,堆内存应该保持平稳而不是持续爬升。
6.4 进程被杀后的恢复:SavedStateHandle
上面讲的都是配置变更,但App被系统杀死是另一回事。进程一旦被回收,ViewModelStore本身也没了,数据必须从外部恢复。Jetpack为此提供了SavedStateHandle,可以在ViewModel创建时注入一个类似Bundle的保存容器,把少量、可序列化的关键状态放进去。
做法是在Factory里使用SavedStateViewModelFactory,或者用by viewModels时的默认工厂,很多情况下会自动支持。然后在ViewModel里可以:
val userId = savedStateHandle.get<String>("userId")等到下次进程重建,这些状态自动恢复。SavedStateHandle非常适合保存当前Tab位置、搜索关键词、表单关键字段,而不适合放大对象或复杂列表,那些还是靠本地缓存恢复更合理。
7. 常见问题与排查技巧实录
7.1 ViewModel到底是不是同一个?
判断方法很直观:在ViewModel构造函数里打日志或计数。转屏两次如果只打印一次,说明是同一个实例。finish再进入,会再打印一次,说明新的生命周期开始。
在Fragment里还要注意宿主是谁。如果两个Fragment挂在同一个Activity下,用requireActivity()作为owner,它们共享一个ViewModel,数据天然互通。如果Fragment自己作为owner,则各自独立。业务上要共享数据,就用Activity作为owner。
7.2 LiveData不回调的排查顺序
遇到LiveData不回调,我一般按这个顺序查:先确认页面是否进入STARTED状态,数据是否真的被set/Post过;再确认是否在子线程setValue抛异常没被看到;然后检查是否用错了变量,比如XML绑定的是旧的observable,代码里改的是新的。还有一个很隐蔽的情况:如果你用observeForever并且在回调里remove了另一个observer,容易引发ConcurrentModification异常,可以改用postValue延迟或加锁。
LiveData的粘性特性也可能让人困惑:新观察者注册后会立刻收到旧值,这常常被误认为“为什么我刚进来就触发一次”。如果你不想要这种粘性,就把事件包装成Event,或者使用Kotlin Flow中的SharedFlow。
7.3 DataBinding生成类找不到怎么办
标准排查流程:确认build.gradle里开了dataBinding true,确认XML根标签是<layout>,确认没有在<data>外引用未定义的变量。然后执行Clean Project -> Rebuild Project。如果还找不到,检查是不是模块间的依赖没搭好,比如Binding类在library模块中生成,但主工程没有依赖这个library。
还有一个小坑:Android Studio的编辑器偶尔会标红找不到Binding类,但只要compileSdkVersion和依赖没问题,Rebuild之后就好了。别被编辑器红波浪线劝退,以Gradle构建结果为准。
7.4 EditText双向绑定不生效
双向绑定不生效,十有八九是把@=写成了@。另一个可能是数据源不是可观察类型。双向绑定要求属性是可被观察且可被反向写回的,用普通String根本编译不过去。
出现这种情况,要么改成MutableLiveData<String>,要么用ObservableField<String>。如果你是自定义View,需要配合InverseBindingAdapter实现反向写回,这个门槛更高,但思路和BindingAdapter类似。
7.5 带参ViewModel在Fragment和Activity中的写法
带参ViewModel的Factory写法前面已经给了例子。在Fragment里,可以用by viewModels外加Factory:
private val vm: UserViewModel by viewModels { UserViewModel.Factory(arguments?.getString("id") ?: "") }还有一种更省代码的方式是使用CreationExtras,在默认工厂里通过SavedStateHandle自动接收arguments。我实际项目里推荐通过SavedStateHandle接收参数,因为进程重建时参数也不会丢,而手工Factory在进程重建时可能会因为参数来源失效而出问题。
7.6 RecyclerView中DataBinding的注意点
列表item用DataBinding时,我在onBindViewHolder里一般这样写:
val binding = ItemUserBinding.bind(itemView) binding.viewModel = itemViewModel binding.executePendingBindings()executePendingBindings是为了让item的绑定数据立刻生效,方便RecyclerView计算高度和避免闪屏。还有一个容易忽略的是,item布局里的监听器可能被复用了,要保证每次绑定时传入的是当前item,不是旧引用。
如果你用DiffUtil + ListAdapter,每次数据变化会多出binding重新绑定的开销,但性能可控。真正要注意的是不要每次onBind都创建一个新的ViewModel或LiveData,这样会造成大量临时对象。Item的ViewModel应该尽量复用,或者干脆只绑定一个纯数据类。
8. 这套架构的边界和后续演进
8.1 什么时候不建议用DataBinding
DataBinding不是银弹。如果页面是完全静态的,或者只有两三个控件交互,直接用ViewBinding甚至findViewById就够。DataBinding的表达式虽然方便,但调试的时候断点难以覆盖到XML内部,出现问题排查路径比较长。
复杂的业务逻辑放到XML表达式里更要克制。一旦表达式中出现方法调用,容易让布局难以阅读、单元测试困难。我见过一个item布局里塞了十几行表达式,改一个需求要翻半天XML。要让XML只做映射,逻辑都进ViewModel或BindingAdapter。
8.2 配套依赖与调试建议
在使用这套架构时,我习惯引入lifecycle-livedata-ktx、lifecycle-viewmodel-ktx、lifecycle-runtime-ktx。如果要做页面级测试,lifecycle-viewmodel-testing里提供InstantTaskExecutor,方便单元测试里控制LiveData的异步派发。
DataBinding和Kotlin协程配合时,可以考虑把网络请求放在ViewModel的viewModelScope里,用LiveData把结果抛给UI。近年Google也推荐用Kotlin Flow替代LiveData,但LiveData在简单场景下的生命周期感知能力还是很有优势。我的原则是:状态恢复类数据适合LiveData,需要事件流和复杂变换的时候切Flow。
8.3 后续还可以尝试的演进路线
如果你的页面进一步复杂化,可以引入StateFlow + Compose,或者继续使用LiveData + DataBinding,两条路线都能走。现在不少新项目已经切到Jetpack Compose,写法更声明式,但我的经验是,现有维护中的项目迁移到Compose成本不小,反而把ViewModel + LiveData + DataBinding这套组合吃透,在中期项目里依然很有价值。
从维护角度,下一步可以逐步把仓库层改成Repository模式,让ViewModel只面向Repository接口取数据,再配合Hilt做依赖注入。总之,页面架构只是起点,数据层、依赖注入层可以按团队节奏慢慢演进。
最后聊点个人经验吧。这套架构真正落地后,我最深的体会是少写了大量胶水代码。以前Activity动不动几百行,现在很多页面代码不超过四五十行,剩下的都在ViewModel和XML声明里。调试的时候,只要想清楚“数据在谁手里”“谁在观察它”“生命周期是否匹配”,绝大多数运行时问题都能快速定位。
如果你刚接触这套组合,建议不要同时引入太多概念。先在项目里把ViewModel用起来,再给列表加LiveData,最后把Activity的XML改成DataBinding。一步步来,遇到问题时排查范围会小很多。我现在带新人,也是用这个顺序带,基本几周就能从只会findViewById过渡到能独立写架构完整的新页面。等你完全适应这种“数据驱动UI”的写法,再回头去看那堆手动同步代码,应该会有种回不去了的感觉。