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 的完整数据流:

Room + LiveData 数据流 UI (Activity/Fragment) 观察 LiveData ViewModel 持有 LiveData Room DAO 返回 LiveData SQLite 数据库 表数据存储 observe() getAllTodos() SQL 查询 数据变化 新数据推送 UI 自动刷新 关键点 Room 自动监听表变化 → 重新执行 SQL → 通过 LiveData 推送新结果 → UI 自动刷新

避坑指南:我曾经在项目里犯过一个低级错误——在 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 的优势在于更丰富的操作符(如 mapcombine),而且不依赖 Android 生命周期。不过 LiveData 在简单场景下更直观,看个人喜好。

好了,这一章的内容就到这里。Room + LiveData 的组合,说白了就是「数据驱动 UI」的最佳实践之一。你只要把 DAO 写好,剩下的交给框架。省心,省力,还不容易出 bug。


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