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