6、LiveData与Room结合:数据库变化自动通知UI
说实话,LiveData 和 Room 的搭配,是我在 Android 开发中最喜欢的组合之一。为什么?因为这两个东西就像天生一对——Room 负责数据持久化,LiveData 负责把数据变化推送到 UI,中间不需要你写任何多余的代码。
我记得刚接触这个组合时,心里还犯嘀咕:「数据库变了,UI 怎么知道?」后来发现,Room 的 DAO 可以直接返回 LiveData 类型,数据库一有变化,LiveData 自动触发更新。嗯,就是这么简单粗暴。
Room DAO 返回 LiveData
先看一个最基础的例子。假设我们有一个用户表:
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int
)
DAO 接口里,查询方法直接返回 LiveData:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY id ASC")
fun getAllUsers(): LiveData<List<User>>
@Insert
suspend fun insertUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
看到没?getAllUsers() 返回的是 LiveData<List<User>>。Room 会在后台自动监听这张表的变化。一旦有 insert、update 或 delete 操作,Room 就会重新执行这条 SQL,然后把新结果通过 LiveData 推出去。
核心原理:Room 内部维护了一个 InvalidationTracker,它会监听所有表的变更。当检测到数据变化时,它会通知对应的 LiveData 重新查询。整个过程是异步的,不会阻塞主线程。
数据库变化自动通知 UI
接下来,我们在 ViewModel 里拿到这个 LiveData,然后 UI 层直接观察它:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val users: LiveData<List<User>> = userDao.getAllUsers()
fun addUser(name: String, age: Int) {
viewModelScope.launch {
val user = User(
id = System.currentTimeMillis().toInt(),
name = name,
age = age
)
userDao.insertUser(user)
}
}
fun removeUser(user: User) {
viewModelScope.launch {
userDao.deleteUser(user)
}
}
}
Activity 或 Fragment 里观察:
class UserActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
val db = AppDatabase.getInstance(this)
val dao = db.userDao()
viewModel = ViewModelProvider(
this,
UserViewModelFactory(dao)
).get(UserViewModel::class.java)
viewModel.users.observe(this) { userList ->
// 数据变了,更新 UI
updateRecyclerView(userList)
}
}
}
这里有个细节——observe() 的第一个参数是 LifecycleOwner。当 Activity 销毁时,观察会自动解除,不会造成内存泄漏。这一点我特别喜欢,省去了手动管理订阅的麻烦。
实战:列表数据实时更新
好,理论说完了,咱们直接上一个完整的实战例子。做一个待办事项列表,支持添加和删除,数据实时刷新。
先定义实体:
@Entity(tableName = "todos")
data class Todo(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val isCompleted: Boolean = false,
val createdAt: Long = System.currentTimeMillis()
)
DAO:
@Dao
interface TodoDao {
@Query("SELECT * FROM todos ORDER BY createdAt DESC")
fun getAllTodos(): LiveData<List<Todo>>
@Insert
suspend fun addTodo(todo: Todo)
@Delete
suspend fun deleteTodo(todo: Todo)
@Query("UPDATE todos SET isCompleted = :completed WHERE id = :id")
suspend fun updateCompletion(id: Int, completed: Boolean)
}
ViewModel:
class TodoViewModel(private val todoDao: TodoDao) : ViewModel() {
val todos: LiveData<List<Todo>> = todoDao.getAllTodos()
fun addTodo(title: String) {
viewModelScope.launch {
val todo = Todo(title = title)
todoDao.addTodo(todo)
}
}
fun deleteTodo(todo: Todo) {
viewModelScope.launch {
todoDao.deleteTodo(todo)
}
}
fun toggleCompletion(todo: Todo) {
viewModelScope.launch {
todoDao.updateCompletion(todo.id, !todo.isCompleted)
}
}
}
UI 层:
class TodoActivity : AppCompatActivity() {
private lateinit var viewModel: TodoViewModel
private lateinit var adapter: TodoAdapter
private lateinit var binding: ActivityTodoBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTodoBinding.inflate(layoutInflater)
setContentView(binding.root)
val db = AppDatabase.getInstance(this)
val dao = db.todoDao()
viewModel = ViewModelProvider(
this,
TodoViewModelFactory(dao)
).get(TodoViewModel::class.java)
adapter = TodoAdapter(
onDelete = { todo -> viewModel.deleteTodo(todo) },
onToggle = { todo -> viewModel.toggleCompletion(todo) }
)
binding.recyclerView.adapter = adapter
viewModel.todos.observe(this) { todoList ->
adapter.submitList(todoList)
}
binding.addButton.setOnClickListener {
val title = binding.editText.text.toString()
if (title.isNotBlank()) {
viewModel.addTodo(title)
binding.editText.text.clear()
}
}
}
}
个人经验:我在项目中遇到过一个问题——当数据库数据量很大时,Room 的 LiveData 每次变化都会重新查询全部数据。如果你的列表有上万条记录,建议配合 Paging 3 使用,或者用 Flow 配合 distinctUntilChanged() 做更细粒度的控制。
核心逻辑流程图
下面这张图展示了 Room + LiveData 的完整数据流:
避坑指南:我曾经在项目里犯过一个低级错误——在 DAO 的查询方法里用了 LiveData,但又在 ViewModel 里手动调用了 postValue()。结果数据更新了两次,UI 闪了一下。记住:Room 返回的 LiveData 是自动管理的,你不需要也不应该手动给它赋值。
为什么这个组合这么香?
说白了,就是「声明式数据流」的体现。你只需要告诉 Room:「我要查什么数据」,然后告诉 UI:「你观察这个 LiveData」。剩下的,Room 帮你搞定。
你想想看,以前我们写数据库操作,要手动开线程、查数据、切回主线程、更新 UI。现在呢?一行 LiveData<List<Todo>> 全搞定。代码量至少减少一半,而且不容易出错。
我个人习惯是:只要涉及本地数据库的列表展示,一律用 Room + LiveData。除非有特殊需求(比如需要做复杂的缓存策略),否则这个组合够用了。
小技巧:如果你用的是 Kotlin,可以考虑用 Flow 替代 LiveData。Room 同样支持返回 Flow<List<T>>。Flow 的优势在于更丰富的操作符(如 map、combine),而且不依赖 Android 生命周期。不过 LiveData 在简单场景下更直观,看个人喜好。
好了,这一章的内容就到这里。Room + LiveData 的组合,说白了就是「数据驱动 UI」的最佳实践之一。你只要把 DAO 写好,剩下的交给框架。省心,省力,还不容易出 bug。
公众号:蓝海资料掘金营,微信deep3321