Search code examples
androidkotlinandroid-jetpack-composeandroid-roomkotlin-coroutines

How to retrieve data from SQLite Room and assign it to a variable in Compose?


How to retrieve a value from the SQLite Database in Android and assign it to a variable? I want to use the previously saved value for a few functions in the code further.

My Database has 3 columns, 1st one is auto-generated int id (key), 2nd one is named type which stores strings that are used to filter out data. The last one is content which also stores string. This third one is actually the one which contains the useful data.

The Dao:

@Upsert
suspend fun upsert(appData: AppData)

@Delete
suspend fun delete(appData: AppData)

@Query("SELECT * from appData WHERE type = :type ORDER BY id DESC")
fun getData(type: String): Flow<List<AppData>>

@Query("SELECT * from appData WHERE type = :type ORDER BY id DESC")
fun getDataList(type: String): List<AppData>

The Repository interface:

fun getDataStream(type: String): Flow<List<AppData?>>
fun getDataListStream(type: String): List<AppData?>

suspend fun upsertData(data: AppData)

suspend fun deleteData(data: AppData)

And this is the Offline Repository implementation:

override fun getDataStream(type: String): Flow<List<AppData?>> = appDataDao.getData(type)
override suspend fun deleteData(data: AppData) = appDataDao.delete(data)
override suspend fun upsertData(data: AppData) = appDataDao.upsert(data)
override fun getDataListStream(type: String): List<AppData?> = appDataDao.getDataList(type)

I tried to retrieve data directly from my composable function and use synchronization methods, but because none of them worked, latest version of my code I is this:

var lastDateObjectList by remember { mutableStateOf(emptyList<AppData>()) }

LaunchedEffect(streakCounterViewModel) {
    coroutineScope {
        streakCounterViewModel.appDataRepository.getDataStream("lastDate")
            .collect { newDataList ->
                lastDateObjectList = newDataList as List<AppData>
            }
    }
}

if (lastDateObjectList.size > 1) {/*TODO*/
    for (i in 1 until lastDateObjectList.size) {
        LaunchedEffect(Unit) {
            streakCounterViewModel.deleteData(
                id = lastDateObjectList[i]!!.id,
                type = lastDateObjectList[i]!!.type,
                content = lastDateObjectList[i]!!.content
            )
        }
    }
}

val lastDateObject: AppData?/* = lastDateObjectList[0] ?: null*/
lastDateObject = if (lastDateObjectList.isNotEmpty())
    lastDateObjectList[0]
else null

var lastDate = lastDateObject?.content ?: "00-00-0000"

Solution

  • Only your view model may access the repository, your composables only access the view model.

    So actually calling getDataStream is done in the view model. Currently you have its type parameter hard-coded to "lastDate", which you probably want to be variable. For that you need to introduce another property in the view model to hold the currently selected type, like this:

    private val selectedType: MutableStateFlow<String?> = MutableStateFlow(null)
    
    fun selectType(type: String) {
        selectedType.value = type
    }
    

    The UI can now change which type should be selected by calling selectType. This will change the value of the property selectedType. We need a flow here so we can map its content to the return value from the repository's getDataStream function like this:

    @OptIn(ExperimentalCoroutinesApi::class)
    val appData: StateFlow<List<AppData>> = selectedType
        .flatMapLatest { type ->
            if (type == null) flowOf(emptyList())
            else appDataRepository.getDataStream(type)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )
    

    The stateIn part at the end converts the flow to a StateFlow. That is needed so that your composable can properly collect it, like this:

    val lastDateObjectList by streakCounterViewModel.appData.collectAsStateWithLifecycle()
    

    You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose for that.

    This replaces the lastDateObjectList variable you had before. You can now also remove the entire LaunchedEffect block.

    In your Repository you had, for some reason, changed the type of the flow to contain null values in your list. This can never happen, so you should change its return type from

    Flow<List<AppData?>>
    

    to

    Flow<List<AppData>>
    

    One last word regarding the getDataList function in your Dao: This function would block the current thread until the database query finished. Never do that. Instead, add the suspend modifier, like this:

    suspend fun getDataList(type: String): List<AppData>
    

    This forces the function to be called from a coroutine that will run asynchronously and not block your main thread.

    Since you already use the dao function that returns a flow (which is preferable) you won't need this function anyways, so just delete it.