Room数据库计数器App递增无效问题排查与解决
开发一款基于Kotlin、Jetpack Compose、Room和Hilt的计数应用,点击递增按钮后计数无变化。Logcat输出如下:
- Incrementing counter...
incrementCount()called withid=0- Loaded count from
DB: 0
以下是代码实现:
Entity
@Entity data class CounterEntity( @PrimaryKey(autoGenerate = true) val id: Int = 0, val count: Int)
Data Access Object
@Dao interface CounterDataAccessObject { @Query("SELECT count FROM CounterEntity WHERE id = :id") suspend fun getCount(id: Int): Int? @Query("UPDATE CounterEntity SET count = count + 1 WHERE id = :id") suspend fun incrementCount(id: Int) { Log.d("CounterDAO", "incrementCount() called with id=$id") } @Query("UPDATE CounterEntity SET count = count - 1 WHERE id = :id") suspend fun decrementCount(id: Int) }
Database
@Database( entities = [CounterEntity::class], version = 1, exportSchema = false ) abstract class CounterDatabase : RoomDatabase() { abstract fun counterDataAccessObject() : CounterDataAccessObject companion object { @Volatile private var INSTANCE: CounterDatabase? = null fun getDatabase(context: Context): CounterDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, CounterDatabase::class.java, "CounterDatabase" ).build() INSTANCE = instance instance } } } }
DataSource
package com.example.counter.app.III_data import com.example.counter.app.IV_database.CounterDataAccessObject import javax.inject.Inject class CounterLocalDataSource @Inject constructor( private val counterDatabaseAccessObject: CounterDataAccessObject ) { // liest aktuellen count suspend fun getCount(): Int { //ensureCounterExists() return counterDatabaseAccessObject.getCount(1) ?: 0 } // erhöht den aktuelle count um 1 suspend fun incrementCount() { //ensureCounterExists() counterDatabaseAccessObject.incrementCount(1) } suspend fun decrementCount() { //ensureCounterExists() counterDatabaseAccessObject.decrementCount(1) } suspend fun resetCount() { counterDatabaseAccessObject.resetCount(1) } }
Repository
package com.example.counter.app.III_data import javax.inject.Inject class CounterRepository @Inject constructor( private val counterLocalDataSource: CounterLocalDataSource ) { suspend fun getCount(): Int { return counterLocalDataSource.getCount() } suspend fun incrementCount() { counterLocalDataSource.incrementCount() } suspend fun decrementCount() { counterLocalDataSource.decrementCount() } suspend fun resetCount() { counterLocalDataSource.resetCount() } }
DI App Module
@Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideCounterDatabase(@ApplicationContext context: Context): CounterDatabase { return CounterDatabase.getDatabase(context) } @Provides @Singleton fun provideCounterDataAccessObject(counterDatabase: CounterDatabase): CounterDataAccessObject { return counterDatabase.counterDataAccessObject() } @Provides @Singleton fun provideCounterLocalDataSource(counterDataAccessObject: CounterDataAccessObject): CounterLocalDataSource { return CounterLocalDataSource(counterDataAccessObject) } @Provides @Singleton fun provideCounterRepository(counterLocalDataSource: CounterLocalDataSource): CounterRepository { return CounterRepository(counterLocalDataSource) } }
View Model
@HiltViewModel class CounterViewModel @Inject constructor( private val counterRepository: CounterRepository ) : ViewModel() { // interner veränderbarer Zustand private val _uiState: MutableStateFlow<CounterUiState> = MutableStateFlow(CounterUiState()) // öffentlicher unveränderlicher Zugriff val uiState: StateFlow<CounterUiState> = _uiState.asStateFlow() // initialen Zählstand aus dem Repository laden init { loadCount() } // Zählstand aus dem Repository laden fun loadCount() { viewModelScope.launch{ val count: Int = counterRepository.getCount() Log.d("CounterViewModel", "Loaded count from DB: $count") _uiState.update { it.copy(count = count) } } } fun increment() { viewModelScope.launch { Log.d("CounterViewModel", "Incrementing counter...") counterRepository.incrementCount() loadCount() } } fun decrement() { viewModelScope.launch { counterRepository.decrementCount() loadCount() } } }
Ui State
data class CounterUiState( val count: Int = 0 )
Screen
@Composable fun CounterScreen( modifier: Modifier = Modifier, count: Int, onIncrement: () -> Unit, onDecrement: () -> Unit, ) { Column( modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "Counter: $count", color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(10.dp)) Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Button( modifier = Modifier.width(80.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, // contentColor = MaterialTheme.colorScheme.onPrimary), // onClick = onDecrement ) { Text("-") } Spacer(modifier = Modifier.width(10.dp)) Button( modifier = Modifier.width(80.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, // contentColor = MaterialTheme.colorScheme.onPrimary), // onClick = onIncrement ) { Text("+") } } } }
Main Activity
@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { CounterTheme( darkTheme = false ) { val counterViewModel: CounterViewModel by viewModels() val state by counterViewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(state) { Log.d("MainActivity", "UI State updated: $state") } Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> CounterScreen( modifier = Modifier.padding(innerPadding), count = state.count, onIncrement = counterViewModel::increment, onDecrement = counterViewModel::decrement, ) } } } } }
My App
@HiltAndroidApp class MyApp : Application() {}
核心问题1:Room DAO方法的默认实现覆盖了SQL执行
给CounterDataAccessObject中的incrementCount方法添加了默认实现(包含Log的花括号代码),Room的规则是:如果给带有@Query注解的方法提供了默认实现,Room会忽略注解中的SQL语句,直接执行你写的默认代码。这就导致点击递增按钮时,根本没有执行更新数据库的SQL,只是打印了一条Log。而且由于Room没有正确处理参数绑定,Log中显示的id=0是参数的默认值,并非你传入的1。
核心问题2:数据库中没有初始化计数记录
CounterLocalDataSource中的ensureCounterExists()方法被注释掉了,导致数据库里从来没有插入过id=1的CounterEntity记录。即使后续修复了DAO的问题,UPDATE语句也找不到对应的行,不会产生任何变化;getCount方法返回null,最终显示默认值0。
解决方案步骤
1. 修复DAO方法,移除默认实现
修改CounterDataAccessObject,删除incrementCount方法的默认代码块,让Room根据@Query注解生成正确的SQL执行逻辑,同时新增插入方法用于初始化记录:
@Dao interface CounterDataAccessObject { @Query("SELECT count FROM CounterEntity WHERE id = :id") suspend fun getCount(id: Int): Int? @Query("UPDATE CounterEntity SET count = count + 1 WHERE id = :id") suspend fun incrementCount(id: Int) // 移除默认实现 @Query("UPDATE CounterEntity SET count = count - 1 WHERE id = :id") suspend fun decrementCount(id: Int) // 新增插入方法,用于初始化记录 @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCounter(counter: CounterEntity) }
2. 实现数据库记录初始化逻辑
在CounterLocalDataSource中恢复并实现ensureCounterExists()方法,确保数据库中存在id=1的计数记录:
class CounterLocalDataSource @Inject constructor( private val counterDatabaseAccessObject: CounterDataAccessObject ) { suspend fun getCount(): Int { ensureCounterExists() // 取消注释 return counterDatabaseAccessObject.getCount(1) ?: 0 } suspend fun incrementCount() { ensureCounterExists() // 取消注释 counterDatabaseAccessObject.incrementCount(1) } suspend fun decrementCount() { ensureCounterExists() // 取消注释 counterDatabaseAccessObject.decrementCount(1) } suspend fun resetCount() { ensureCounterExists() counterDatabaseAccessObject.insertCounter(CounterEntity(id = 1, count = 0)) } // 实现初始化方法 private suspend fun ensureCounterExists() { val existingCount = counterDatabaseAccessObject.getCount(1) if (existingCount == null) { counterDatabaseAccessObject.insertCounter(CounterEntity(id = 1, count = 0)) } } }
3. 可选:添加数据库创建时的预填充逻辑(优化)
如果希望应用首次启动时就初始化计数记录,可以在CounterDatabase中添加Callback,在数据库创建时插入初始数据:
@Database( entities = [CounterEntity::class], version = 1, exportSchema = false ) abstract class CounterDatabase : RoomDatabase() { abstract fun counterDataAccessObject() : CounterDataAccessObject companion object { @Volatile private var INSTANCE: CounterDatabase? = null fun getDatabase(context: Context): CounterDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, CounterDatabase::class.java, "CounterDatabase" ) .addCallback(object : RoomDatabase.Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) // 在数据库创建时插入初始记录 GlobalScope.launch { getDatabase(context).counterDataAccessObject() .insertCounter(CounterEntity(id = 1, count = 0)) } } }) .build() INSTANCE = instance instance } } } }
内容的提问来源于stack exchange,提问作者Benedikt Schlüter

