引言:为什么一个"简单的SELECT"会变成难题
\n\n您正在使用 Room 和 Kotlin 开发您的第一个 Android 应用。一切进展顺利,直到您需要从数据库中读取数据。您写了类似这样的代码:
\n\nval user = userDao.getUserById(42)\ntextView.text = user.name\n\n运行应用时,应用崩溃并抛出异常:
\n\njava.lang.IllegalStateException: Cannot access database on the main thread\nsince it may potentially lock the UI for a long period of time.\n\n熟悉吗?这个在 Stack Overflow 上的问题已经获得了数千次浏览和数百条评论。这个错误吓到了很多初学者,但实际上这不是一个 bug —— 这是一种防错保护机制。Room(与其他合格的 ORM 一样)防止您意外阻塞用户界面。
\n\n在本文中,我们将分析这为什么会发生,以及如何正确在 Android Room 配合 Kotlin 执行 SELECT 查询。您将学到三种经过验证的方法:LiveData、协程和 RxJava。让我们开始吧!
\n\n核心问题:为什么 Room 会阻塞主线程?
\n\nAndroid 默认是单线程系统。所有 UI 都在主线程上运行。如果您在主线程上开始读取数据库,界面将冻结直到查询完成。如果数据量大,这可能需要几秒钟——用户将看到一个"卡死"的应用。
\n\nRoom 采取了稳妥的做法:它禁止在主线程上执行任何长时间操作(读取、写入)。除非您在构建数据库时通过 .allowMainThreadQueries() 显式允许这样做。但您不应该这样做——这是一种反模式。
以下是一个典型问题的样子:
\n\n// DAO\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User // suspend — 已经是一个提示!\n}\n\n// Activity\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n // ❌ 错误!\n val user = database.userDao().getUserById(42)\n }\n}\n\n即使您让函数成为 suspend 函数,但如果没有协程就调用它——Room 仍然会抛出异常。我们来修复它!
方法 1:LiveData — 简单且响应式
\n\n最简单的方法是让 DAO 返回 LiveData。Room 自己管理线程:查询在后台运行,结果传到 UI。
\n\n步骤 1. 修改 DAO
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n fun getUserById(userId: Int): LiveData<User>\n}\n\n步骤 2. 在 Activity/Fragment 中订阅
\n\nclass MainActivity : AppCompatActivity() {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n\n database.userDao().getUserById(42).observe(this) { user ->\n textView.text = user.name\n }\n }\n}\n\n优点: 代码最少,当 Activity 销毁时自动取消订阅,具有响应性——当表变化时数据自动更新。
\n\n缺点: LiveData 与生命周期绑定,不容易与其他数据源(网络、SharedPreferences)组合。
\n\n方法 2:协程 — 现代的 Kotlin 方式
\n\n如果您正在使用 Kotlin 协程(您应该使用),这是最优雅的方式。函数标记为 suspend,调用包裹在具有正确 Dispatchers.IO 的协程中。
步骤 1. 带 suspend 的 DAO
\n\n@Dao\ninterface UserDao {\n @Query("SELECT * FROM users WHERE id = :userId")\n suspend fun getUserById(userId: Int): User\n}\n\n步骤 2. 从 ViewModel 调用(推荐)
\n\nclass MainVi