第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"嘿,数据变了,快刷新"。这就是响应式编程的魅力。

笔记应用数据流闭环架构 UI / Activity DataBinding 绑定 ViewModel 持有 LiveData Repository 数据操作封装 Room SQLite 观察 LiveData 自动更新 UI 返回 LiveData 查询结果 数据流闭环:操作向下,数据向上

第一步:定义数据模型和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 = viewModelbinding.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会导致内存泄漏。

好了,这一章的内容就到这里。代码都在上面了,建议你照着敲一遍,跑起来试试。只有亲手写过一遍,才能真正理解这个数据流闭环是怎么转起来的。

公众号:蓝海资料掘金营,微信 deep3321