Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

java.lang.IllegalStateException when collecting flow from SqlDelight in ViewModel

I am trying to use SqlDelight database in my app.

In my DAO, I have a function called getRecipeById to query the database and return a flow of domain model (Recipe). Here is the implementation of the function: (Note: RecipeTable is the name of the table, or I guess I should have called it RecipeEntity)

fun getRecipeById(id: Int): Flow<Recipe> {
    return recipeQueries.getRecipeById(id)
        .asFlow()
        .mapToOne()
        .map { recipeTable: RecipeTable ->
            recipeTableToRecipeMapper(recipeTable = recipeTable)
        }
}

Then my repository calls this function like this: (Note: recipeLocalDatabase is my DAO)

suspend fun getRecipeById(id: Int): Flow<RecipeDTO> {
    val recipeFromDB: Flow<Recipe> = recipeLocalDatabase.getRecipeById(id)
    return recipeFromDB.map { recipe: Recipe ->
        recipeDTOMapper.mapDomainModelToDTO(recipe)
    }
}

Then my usecase layer calls the function from repository and pass along the flow:

suspend fun execute(
    id: Int
): UseCaseResult<Flow<RecipeDTO>>
{
    return try{
        UseCaseResult.Success(recipeRepository.getRecipeById(id))
    } catch(exception: Exception){
        UseCaseResult.Error(exception)
    }
}

Now within the ViewModel, I am collecting the flow of RecipeDTO like this:

var recipeForDetailView: MutableState<RecipeDTO> = mutableStateOf(
    RecipeDTO(
        id  = 0,
        title = "",
        featuredImage = "",
        ingredients = listOf()
    )
)

fun onLaunch(recipeId: Int){
    viewModelScope.launch(Dispatchers.IO) {
        when(val result = getRecipeDetailUseCase.execute(recipeId))
        {
            is UseCaseResult.Success ->
                 result.resultValue.collect{ recipeDTO: RecipeDTO ->
                     recipeForDetailView.value = recipeDTO
                 }
            is UseCaseResult.Error -> Log.d("Debug: RecipeDetailViewModel",
                result.exception.toString()
            )
        }
    }
}

My View layer will then observe the MutableState object. Now, I keep getting this error every once a while

E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-5
Process: com.noat.recipe_food2fork, PID: 13923
java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
    at androidx.compose.runtime.snapshots.SnapshotKt.readError(Snapshot.kt:1530)
    at androidx.compose.runtime.snapshots.SnapshotKt.current(Snapshot.kt:1770)
    at androidx.compose.runtime.SnapshotMutableStateImpl.setValue(SnapshotState.kt:891)
    at com.noat.recipe_food2fork.ui.viewModel.RecipeDetailViewModel$onLaunch$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:133)
    at com.noat.recipe_food2fork.data.repositoryImplementation.recipeRepository.RecipeRepository$getRecipeById$$inlined$map$1$2.emit(Collect.kt:135)
    at com.noat.recipe_food2fork.data.local.RecipeLocalDatabase$getRecipeById$$inlined$map$1$2.emit(Collect.kt:135)
    at com.squareup.sqldelight.runtime.coroutines.FlowQuery$mapToOne$$inlined$map$1$2.emit(Collect.kt:135)
    at com.squareup.sqldelight.runtime.coroutines.FlowQuery$mapToOne$$inlined$map$1$2$1.invokeSuspend(Unknown Source:12)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

I think it is the flow collecting that caused the error, or maybe because of the fact that I am using MutableState? Googling the error does not help much... Does anyone know what is the cause of this issue? Thank you! Sorry for the long long post.

like image 471
Toan Pham Avatar asked Sep 18 '25 12:09

Toan Pham


1 Answers

I don't think MutableState is designed to be used in the ViewModel layer, since it's an observable integrated with the compose runtime. You could create a MutableStateFlow instead and use collectAsState() from the view layer.

In your case the issue is probably, because of the state is captured in a coroutine invoked outside composition.

like image 59
Róbert Nagy Avatar answered Sep 21 '25 02:09

Róbert Nagy