添加Stat子实体时触发java.lang.IllegalStateException空值异常求助
问题排查与解决方案
问题描述
Android应用中,用户拥有多个Stat实体,添加新Stat时偶尔随机抛出以下异常:
java.lang.IllegalStateException: Required value was null.
完整错误栈:
FATAL EXCEPTION: DefaultDispatcher-worker-3 Process: com.apps.irlleveler, PID: 15274 java.lang.IllegalStateException: Required value was null. at com.apps.irlleveler.data.db.UserDao_Impl.__fetchRelationshipStatAscomAppsIrllevelerDataStatWithTasks(UserDao_Impl.kt:273) at com.apps.irlleveler.data.db.UserDao_Impl.getUserWithStatsWithTasks$lambda$1(UserDao_Impl.kt:83) at com.apps.irlleveler.data.db.UserDao_Impl.$r8$lambda$f2zhaSZJKsl9DSGFGRABt37Yfa0(Unknown Source:0) at com.apps.irlleveler.data.db.UserDao_Impl$$ExternalSyntheticLambda2.invoke(D8$$SyntheticClass:0) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend(DBUtil.kt:68) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:8) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:4) at androidx.room.driver.SupportSQLiteConnectionPool.useConnection(SupportSQLiteConnectionPool.android.kt:42) at androidx.room.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:126) at androidx.room.RoomDatabase.useConnection$room_runtime_release(RoomDatabase.android.kt:593) at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend(DBUtil.android.kt:113) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) at java.lang.Thread.run(Thread.java:1012) Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@79f54d6, Dispatchers.IO]
推测是插入Stat时无法找到关联User实体导致,但实际根源不止于此。
核心问题分析
- Stat实体设计错误
- Stat类中把
User对象作为@ColumnInfo字段,Room无法直接持久化复杂对象,会导致序列化/反序列化异常,最终关联查询时出现null owningUserId定义为Long,但User的id是Int,类型不匹配,Room关联查询时无法正确匹配owningUserId的默认值依赖owner.id,但插入时如果owner未正确初始化,会导致该值为null或无效
- Stat类中把
- Flow使用不当导致竞态条件
loadUser方法中,先获取Flow再调用first(),然后插入新用户后重新获取Flow,这种方式可能导致并发场景下数据不一致,出现User还未插入完成就执行关联查询的情况
- 关联查询的非空约束
UserWithStatsWithTheStatsTasks中的user字段是非空的,但getUserWithStatsWithTasks()返回的是Flow<UserWithStatsWithTheStatsTasks?>,当查询不到User时会返回null,但后续转换UI模型时未做充分判空
解决方案
1. 修复Stat实体结构
移除owner字段,改用owningUserId作为外键关联User,确保类型一致:
@Entity( foreignKeys = [ForeignKey( entity = User::class, parentColumns = ["id"], childColumns = ["owningUserId"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE )] ) data class Stat ( @PrimaryKey(autoGenerate = true) val id: Int = 0, @ColumnInfo val name: String, @ColumnInfo val description: String = "", @ColumnInfo val level: Int = 1, @ColumnInfo val totalXP: Int = 0, @ColumnInfo val currentXP: Int = 0, @ColumnInfo val neededXP: Int = 1, @ColumnInfo val owningUserId: Int = 0 // 和User的id类型保持一致 )
2. 修正UserDao的查询逻辑
用@Transaction确保关联查询的原子性:
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: User): Long @Transaction @Query("SELECT * FROM user WHERE id = 0") fun getUserWithStatsWithTasks(): Flow<UserWithStatsWithTheStatsTasks?> }
3. 修复ViewModel中的竞态问题
优化loadUser方法,避免重复获取Flow,确保User插入完成后再更新UI:
private fun loadUser() { viewModelScope.launch(Dispatchers.IO) { val userFlow = userDao.getUserWithStatsWithTasks() val initialUser = userFlow.firstOrNull() if (initialUser?.user == null) { val newUser = User(username = "Player") userDao.insertUser(newUser) // 插入后直接收集最新数据 userFlow.firstOrNull()?.let { _uiUser.value = it.toUIModel() } } else { _uiUser.value = initialUser.toUIModel() } } }
4. 确保外键约束生效
在Room数据库构建时直接启用外键约束:
instance = Room.databaseBuilder( context, AppDatabase::class.java, NAME ) .allowMainThreadQueries() // 建议仅调试用,正式版移除 .setForeignKeysEnforced(true) // 直接启用外键约束 .build()
5. 修正StatViewModel的插入逻辑
插入Stat时确保传入正确的owningUserId(从当前用户获取):
// 在StatViewModel中依赖UserViewModel获取当前用户ID class StatViewModel(private val userViewModel: UserViewModel) : ViewModel() { val statDao = MainApplication.appDatabase.getStatDao() fun insertStat(statName: String, description: String = ""){ viewModelScope.launch(Dispatchers.IO) { val currentUserId = userViewModel.uiUser.value?.id ?: 0 val stat = Stat( name = statName, description = description, owningUserId = currentUserId ) statDao.insertStat(stat) // 插入后通知UserViewModel刷新数据 userViewModel.reloadUser() } } }
内容的提问来源于stack exchange,提问作者Nickname
相关产品推荐
相关产品推荐

