Jetpack Compose中ViewModel结合Room数据库导致应用启动崩溃问题求助
Jetpack Compose中ViewModel结合Room数据库导致应用启动崩溃问题求助
我现在遇到一个头疼的问题——当我打开调用ViewModel的Composable视图时,应用直接崩溃了。我怀疑是ViewModel里的Room数据库配置出了问题,但之前听说直接给ViewModel传Context不是最佳实践,所以现在有点迷茫。下面是我的代码和错误日志,麻烦大家帮我看看哪里出问题了!
我的现有代码
ViewModel 代码
class DataModel(): ViewModel() { lateinit var database: AppDatabase fun setDatabase(context: Context) { database = Room.databaseBuilder( context, AppDatabase::class.java, "database" ).build() } val data: StateFlow<List<Data>> = flow { emit(database.dataDao().getAll()) } as StateFlow<List<Data>> fun addData(data: Data) { viewModelScope.launch { database.dataDao().insert(data) } } }
Room Database 代码
@Database(entities = [Data::class], version = 1) @TypeConverters(BooleanArrayConverter::class) abstract class AppDatabase: RoomDatabase() { abstract fun dataDao(): DataDao // 原代码写的是data(),已修正为对应DAO接口的命名 }
DAO 接口代码
@Dao interface DataDao { @Query("SELECT * FROM data") suspend fun getAll(): List<Data> @Insert suspend fun insert(vararg data: Data) }
Composable 视图代码
fun View( dataModel: DataModel = viewModel(), ) { val context = LocalContext.current Button( onClick = { dataModel.setDatabase(context) }, ) { Text("Button") } }
崩溃错误日志
FATAL EXCEPTION: main Process: com.example.app, PID: 12733 java.lang.RuntimeException: java.lang.reflect.InvocationTargetException at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:603) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:932) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at androidx.lifecycle.viewmodel.internal.JvmViewModelProviders.createViewModel(JvmViewModelProviders.kt:38) at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.android.kt:185) at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.android.kt:309) at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.android.kt:269) at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.android.kt:142) at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.android.kt:112) at androidx.lifecycle.viewmodel.ViewModelProviderImpl_androidKt.createViewModel(ViewModelProviderImpl.android.kt:34) at androidx.lifecycle.viewmodel.ViewModelProviderImpl.getViewModel$lifecycle_viewmodel_release(ViewModelProviderImpl.kt:60) at androidx.lifecycle.viewmodel.ViewModelProviderImpl.getViewModel$lifecycle_viewmodel_release$default(ViewModelProviderImpl.kt:43) at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.android.kt:92) at androidx.lifecycle.viewmodel.compose.ViewModelKt__ViewModelKt.get(ViewModel.kt:172) at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(Unknown Source:1) at androidx.lifecycle.viewmodel.compose.ViewModelKt__ViewModelKt.viewModel(ViewModel.kt:106) at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(Unknown Source:1) at com.example.app.View.NewJournalEntrySheet(View.kt:174) at com.example.app.ComposableSingletons$ViewKt$lambda-1$1.invoke(JournalEntryScreen.kt:45) at com.example.app.ComposableSingletons$ViewKt$lambda-1$1.invoke(JournalEntryScreen.kt:44) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:130) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:51) at androidx.compose.material3.ModalBottomSheetKt$ModalBottomSheetContent$7.invoke(ModalBottomSheet.kt:341) at androidx.compose.material3.ModalBottomSheetKt$ModalBottomSheetContent$7.invoke(ModalBottomSheet.kt:289) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:51) at androidx.compose.material3.SurfaceKt$Surface$1.invoke(Surface.kt:126) at androidx.compose.material3.SurfaceKt$Surface$1.invoke(Surface.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:51) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:364) at androidx.compose.material3.SurfaceKt.Surface-T9BRK9s(Surface.kt:105) at androidx.compose.material3.ModalBottomSheetKt.ModalBottomSheetContent-IQkwcL4(ModalBottomSheet.kt:218) at androidx.compose.material3.ModalBottomSheetKt$ModalBottomSheet$3.invoke(ModalBottomSheet.kt:175) at androidx.compose.material3.Modal...
我自己的猜测
我本来想在ViewModel里创建数据库实例,但又知道直接传Context给ViewModel不是好做法,所以才搞了个按钮点击来初始化数据库,但没想到还没点按钮应用就崩了...
问题根源分析
后来我才反应过来,崩溃的核心原因其实很直白:当ViewModel被创建的时候,data这个StateFlow就会立即初始化,而在初始化Flow的Lambda里,我直接调用了database.dataDao().getAll(),但此时database还是lateinit修饰的空值(要等点击按钮才会初始化),这就直接触发了未初始化属性访问的异常,最终导致应用崩溃。
另外还有几个隐藏的小问题:
- 直接给ViewModel传Context确实存在内存泄漏风险,不是最佳实践;
- 把普通Flow强转成StateFlow的写法不规范,应该用官方推荐的
stateIn操作符; - 原
AppDatabase的抽象方法命名和DAO接口不匹配,会导致编译错误。
解决方案
我整理了两个可行的方案,大家可以根据项目规模选择:
方案一:Application层创建数据库单例(简单易上手,适合小型项目)
这种方式不需要额外依赖,直接在Application类里初始化数据库单例,ViewModel直接复用这个单例:
- 自定义Application类:
class MyApp: Application() { companion object { lateinit var database: AppDatabase } override fun onCreate() { super.onCreate() // 在Application启动时初始化数据库 database = Room.databaseBuilder( applicationContext, AppDatabase::class.java, "database" ).build() } }
- 在
AndroidManifest.xml里注册这个Application:
<application android:name=".MyApp" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" ...> ... </application>
- 修改ViewModel:
class DataModel(): ViewModel() { private val database = MyApp.database // 用stateIn正确创建StateFlow val data: StateFlow<List<Data>> = flow { emit(database.dataDao().getAll()) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), // 订阅者消失5秒后停止数据流 initialValue = emptyList() // 初始值设为空列表,避免UI空指针 ) fun addData(data: Data) { viewModelScope.launch { database.dataDao().insert(data) } } }
- 简化Composable视图:
fun View( dataModel: DataModel = viewModel(), ) { // 观察数据状态更新UI val dataList by dataModel.data.collectAsState() // 示例:展示数据列表 LazyColumn { items(dataList) { item -> Text(text = item.toString()) } } }
方案二:使用Hilt依赖注入(推荐的最佳实践,适合中大型项目)
用Hilt管理依赖可以彻底解决ViewModel依赖Context的问题,同时符合Jetpack架构规范:
- 按照官方文档添加Hilt的依赖和基础配置;
- 创建Database模块提供依赖:
@Module @InstallIn(SingletonComponent::class) object DatabaseModule { @Provides fun provideDataDao(database: AppDatabase): DataDao { return database.dataDao() } @Provides @Singleton fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder( context, AppDatabase::class.java, "database" ).build() } }
- 修改ViewModel,用Hilt注入DAO:
@HiltViewModel class DataModel @Inject constructor( private val dataDao: DataDao // 直接注入DAO,无需关心数据库实例 ): ViewModel() { val data: StateFlow<List<Data>> = flow { emit(dataDao.getAll()) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun addData(data: Data) { viewModelScope.launch { dataDao.insert(data) } } }
- 简化Composable视图:
@Composable fun View( dataModel: DataModel = hiltViewModel(), ) { val dataList by dataModel.data.collectAsState() // 你的UI逻辑... }
内容来源于stack exchange
相关产品推荐
相关产品推荐

