You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Room数据库计数器App递增无效问题排查与解决

问题描述

开发一款基于Kotlin、Jetpack Compose、Room和Hilt的计数应用,点击递增按钮后计数无变化。Logcat输出如下:

  • Incrementing counter...
  • incrementCount() called with id=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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 21:47:28