第28章:LiveData与Room + ViewModel + DataBinding综合实战
说实话,前面讲了那么多理论,今天终于到了我最喜欢的环节——把所有的东西串起来,做一个真正能用的笔记应用。你想想看,LiveData、Room、ViewModel、DataBinding,这四个组件单独拎出来都挺能打的,但把它们组合在一起,才是Android官方推荐的全家桶方案。
我在公司带团队的时候,经常看到新人把数据层和UI层搅在一起,Activity里直接写数据库查询,结果代码改起来跟拆炸弹似的。嗯,今天这个实战,就是要让你彻底告别那种写法。
整体架构:数据流闭环长什么样?
先别急着写代码,咱们把架构理清楚。这个笔记应用的核心数据流,说白了就是一个闭环:
UI操作 → ViewModel → Repository → Room → LiveData → UI自动更新
用户点一下"新增笔记",ViewModel收到请求,调用Repository往Room里插数据。Room操作完成后,通过LiveData把最新的数据列表推回来,UI自动刷新。整个过程你不需要手动通知Adapter"嘿,数据变了,快刷新"。这就是响应式编程的魅力。
第一步:定义数据模型和DAO
先写Entity。我习惯把笔记模型设计得简单一点,但该有的字段一个不能少:
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val content: String,
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis()
)
注意这个autoGenerate = true,主键自增。我在项目里见过有人手动传ID,结果插入重复数据直接崩了。嗯,交给Room自己处理最省心。
接下来是DAO。这里有个关键点——查询方法返回LiveData:
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
fun getAllNotes(): LiveData<List<Note>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: Note): Long
@Update
suspend fun update(note: Note)
@Delete
suspend fun delete(note: Note)
}
个人经验:我建议所有写操作都用suspend函数。为什么?因为Room的写操作默认是在后台线程执行的,但如果你在UI线程直接调,会抛异常。用协程+挂起函数,既安全又简洁。
第二步:Repository——数据操作的守门员
Repository的作用,说白了就是给ViewModel一个干净的接口。ViewModel不需要知道数据是从Room来的,还是从网络来的:
class NoteRepository(private val noteDao: NoteDao) {
val allNotes: LiveData<List<Note>> = noteDao.getAllNotes()
suspend fun insert(note: Note) {
noteDao.insert(note)
}
suspend fun update(note: Note) {
noteDao.update(note.copy(updatedAt = System.currentTimeMillis()))
}
suspend fun delete(note: Note) {
noteDao.delete(note)
}
}
看到note.copy(updatedAt = ...)这行了吗?更新的时候顺手把时间戳改了。我曾经在一个项目里忘了更新这个字段,结果列表排序全乱了,排查了半天才发现是updatedAt没变。这种小细节,踩过一次坑就记住了。
第三步:ViewModel——UI的贴心管家
ViewModel持有LiveData,对外暴露不可变的版本。这样外部只能观察,不能篡改:
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository
val allNotes: LiveData<List<Note>>
init {
val noteDao = NoteDatabase.getInstance(application).noteDao()
repository = NoteRepository(noteDao)
allNotes = repository.allNotes
}
fun insert(title: String, content: String) {
viewModelScope.launch {
repository.insert(Note(title = title, content = content))
}
}
fun update(note: Note, title: String, content: String) {
viewModelScope.launch {
repository.update(note.copy(title = title, content = content))
}
}
fun delete(note: Note) {
viewModelScope.launch {
repository.delete(note)
}
}
}
注意:千万不要在ViewModel里直接暴露MutableLiveData给外部。我见过有人图省事,把MutableLiveData设成public,结果Fragment里直接调setValue(),数据流全乱套了。记住,对外只给LiveData,写操作统一走函数。
第四步:DataBinding——把数据和UI焊在一起
DataBinding的好处,就是不用写一堆findViewById和setText。咱们直接在布局里绑定:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="viewModel" type="com.example.app.NoteViewModel" />
</data>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{viewModel.allNotes}" />
</layout>
等等,app:items这个属性怎么来的?需要写一个BindingAdapter:
@BindingAdapter("items")
fun setItems(recyclerView: RecyclerView, notes: List<Note>?) {
notes?.let {
(recyclerView.adapter as? NoteAdapter)?.submitList(it)
}
}
这样只要LiveData的数据变了,RecyclerView自动刷新。你想想看,以前写Adapter.notifyDataSetChanged()要手动调,现在全自动了。
第五步:Activity里组装起来
最后一步,在Activity里把ViewModel和DataBinding连上:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: NoteViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
关键就两行:binding.viewModel = viewModel 和 binding.lifecycleOwner = this。前者把ViewModel传给布局,后者告诉DataBinding用Activity的生命周期来管理LiveData的观察。少了lifecycleOwner,LiveData不会自动取消注册,内存泄漏等着你。
完整CRUD操作演示
咱们来走一遍完整的增删改查流程:
| 操作 | 用户触发 | ViewModel处理 | Room执行 | UI反馈 |
|---|---|---|---|---|
| 新增 | 点击保存按钮 | 调用insert() | INSERT语句 | 列表自动新增一条 |
| 修改 | 编辑内容后保存 | 调用update() | UPDATE语句 | 列表对应项刷新 |
| 删除 | 滑动删除或点删除 | 调用delete() | DELETE语句 | 列表自动移除该项 |
| 查询 | 打开应用 | 观察allNotes | SELECT语句 | 列表展示所有笔记 |
你看,整个流程里,Activity和Fragment几乎不用写什么逻辑。ViewModel负责调度,Room负责存储,LiveData负责通知,DataBinding负责渲染。各司其职,清清楚楚。
核心要点:这个架构最妙的地方在于,数据流是单向的。用户操作往下走,数据变化往上推。永远不会出现"UI改了数据,数据又反过来改UI"的死循环。我在好几个商业项目里都用这套模式,维护成本低得惊人。
避坑指南
最后分享几个我踩过的坑:
- LiveData在Fragment里要配合viewLifecycleOwner:如果你在Fragment里观察LiveData,记得用
viewLifecycleOwner而不是this。否则Fragment视图重建时,观察者还绑着旧的Lifecycle,轻则重复触发,重则崩溃。 - Room的数据库版本迁移:加字段的时候别忘了写Migration。我曾经偷懒用了
fallbackToDestructiveMigration(),结果用户升级后数据全没了,被骂惨了。 - 不要在ViewModel里持有Context:除非你用AndroidViewModel,否则别传Context。ViewModel的生命周期比Activity长,持有Context会导致内存泄漏。
好了,这一章的内容就到这里。代码都在上面了,建议你照着敲一遍,跑起来试试。只有亲手写过一遍,才能真正理解这个数据流闭环是怎么转起来的。