Search code examples
androidkotlinstateandroid-room

Android Kotlin Jetpack Compose - Room DB State Problem


I implemented a Room DB and have this problem now with my State:

My ViewModel

@HiltViewModel
class WorkingDayViewModel @Inject constructor(
    private val repository: WorkingDayRepository
) : ViewModel() {

    companion object{
        private const val MILLS = 5_000L
    }

    val allWorkingDaysState : StateFlow<WorkingDayState> = repository.getAllWorkingDays()
        .map { WorkingDayState(it) }
        .onEach {
            println("All Working Days: $it")
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(MILLS),
            initialValue = WorkingDayState()
        )

    fun upsertWorkingDay(workingDay: WorkingDay) {
        viewModelScope.launch {
            repository.upsertWorkingDay(workingDay)
        }
    }
}

My @Composable

 val workingDays by viewModelWorkingDays.allWorkingDaysState.collectAsState()
        LaunchedEffect(key1 = workingDays) {
            Log.d("workingDays", workingDays.toString())
        }

WorkingDay.kt

@Entity(tableName = "WorkingDays")
data class WorkingDay(

    @PrimaryKey(autoGenerate = false)
    val date: LocalDate,

    val startTime: LocalTime,
    val endTime: LocalTime,
    val pauseTime: Int,
    val fixedHourDayDuration: Int,
    val isFixedHourDay: Boolean,
    var isActive: Boolean,
)

WorkingDayState.kt

data class WorkingDayState(
    val workingDays: List<WorkingDay> = emptyList(),
    val date: LocalDate? = null,
    val startTime: LocalTime? = null,
    val endTime: LocalTime? = null,
    val pauseTime: String = "",
    val fixedHourDayDuration: Int = 0,
    val isFixedHourDay: Boolean = false,
    val isActive: Boolean = true,
)

WorkingDayDao.kt

@Dao
interface WorkingDaysDao {

    @Upsert
    fun upsertWorkingDay(workingDay: WorkingDay)

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun initInsertWorkingDay(workingDay: WorkingDay)

    @Delete
    fun deleteWorkingDay(workingDay: WorkingDay)

    @Query("SELECT * FROM workingdays WHERE date = :date")
    fun getWorkingDayByDate(date: LocalDate): Flow<WorkingDay>

    @Query("SELECT * FROM workingdays")
    fun getAllWorkingDays(): Flow<List<WorkingDay>>

}

WorkingDayRepository.kt

class WorkingDayRepository @Inject constructor(
    private val dao: WorkingDaysDao
) {

    fun upsertWorkingDay(workingDay: WorkingDay) = dao.upsertWorkingDay(workingDay)
    fun initInsertWorkingDay(workingDay: WorkingDay) = dao.initInsertWorkingDay(workingDay)
    fun deleteWorkingDay(workingDay: WorkingDay) = dao.deleteWorkingDay(workingDay)
    fun getWorkingDayByDate(date: LocalDate) = dao.getWorkingDayByDate(date)
    fun getAllWorkingDays() = dao.getAllWorkingDays()

}

TypeConverters

class LocalDateConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): LocalDate? {
        return value?.let {
            ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate()
        }
    }

    @TypeConverter
    fun dateToTimestamp(date: LocalDate?): Long? {
        return date?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
    }
}

class LocalTimeConverter {
    @TypeConverter
    fun fromLocalTime(value: LocalTime?): String? {
        return value?.toString()
    }

    @TypeConverter
    fun toLocalTime(value: String?): LocalTime? {
        return value?.let { LocalTime.parse(it) }
    }
}

Project on Git: https://github.com/SkAppCoding/DebugTests

The Problem is now as followed:

When I call upsertWorkingDay and change some values. The values get saved in the DB. allWorkingDaysState also emitts a new state. the Log in .onEach is printed. But here is where the Problem starts. My LaunchedEffect is not triggered.

The other thing is. When I change a value in the DB directly with "App Inspection" in Android Studio, allWorkingDaysState also emitts a new value. the Log in .onEach is printed. But also my LaunchedEffect is triggered.

Where is the difference. What I am doing wrong? I hope you can help me.

I tried a lot of Log messages. But nothing helped me to find the problem.

I expect that my LaunchedEffect is also triggered if allWorkingDaysState emitts a new value after upsertWorkingDay.


Solution

  • The most important part is missing from your example code, namely, how you update your database.

    From the code you provided only one thing looks conspicuous: Your Room entity contains the property isActive that is declared as var. This is probably an accident since all other fields are properly declared as val (as it is necessary when using StateFlows and Compose, objects must be immutable), but this one may tempt you to actually change that value. This is the only case where I can image the behavior you described can occur.

    Change it to val and everything should be fine. When you want to change isActive, do it the same way as with all the other val properties: Use copy to create a new object.

    val changedWorkingDay = workingDay.copy(isActive = !workingDay.isActive)
    

    If you would modify the old workingDay object Compose would erroneously assume the new value is the same as the old value because all their properties match, and won't trigger a recomposition, never updating the UI.