Search code examples
androidkotlinandroid-jetpack-composeandroid-room

Why do the functions from my repository fail to access the data from my room database?


I have created a room database in my app and I can add data to it with the functions I have written, but I cannot retrieve them. My goal is simply to save usernames and email and retrieve them when they are modified so that I can display the change in my UI. The problem is that my functions do not retrieve the info and simply return the default value like null or empty strings.

Here is my entity:

@Entity
data class Info(
    val email: String,
    val userName: String,
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
)

Here is my dao:

interface InfoDao {
    @Upsert
    suspend fun upsertInfo(info: Info)

    @Delete
    suspend fun deleteInfo(info: Info)

    @Query("SELECT userName FROM info LIMIT 1")
    fun getUsernameAsLiveData(): LiveData<String>

    @Query("SELECT email FROM info LIMIT 1")
    fun getEmailAsLiveData(): LiveData<String>
}

My repository:

class Repo(
    private val db: InfoDatabase,
) {
    fun openDatabase() {
        db.openHelper.writableDatabase
    }

    suspend fun upsertInfo(info: Info) {
        db.dao.upsertInfo(info)
    }

    suspend fun deleteInfo(info: Info) {
        db.dao.deleteInfo(info)
    }

    fun getUsername(): LiveData<String> {
        val usernameLiveData = db.dao.getUsernameAsLiveData()
        return usernameLiveData
    }

    fun getEmail(): LiveData<String> = db.dao.getEmailAsLiveData()
}

My viewModel:

class RoomViewModel(
    private val repo: Repo,
) : ViewModel() {
    private val _isOpen = MutableStateFlow(false)
    val isOpen: StateFlow<Boolean> = _isOpen

    private val _usernameState = MutableLiveData<String?>()
    val usernameState: LiveData<String?> = _usernameState

    private val _emailState = MutableLiveData<String?>()
    val emailState: LiveData<String?> = _emailState

    fun openDatabase() {
        viewModelScope.launch {
            repo.openDatabase()
            _isOpen.value = true
        }
    }

    suspend fun fetchData() {
        viewModelScope.launch {
            if (!isOpen.value) {
                repo.openDatabase()
            }

            val username = repo.getUsername().value
            _usernameState.value = username

            val email = repo.getEmail().value
            _emailState.value = email
        }
    }

    suspend fun upsertInfo(info: Info) {
        viewModelScope.launch {
            repo.upsertInfo(info)
        }
    }
}

And my composable:

@Composable
fun DeviceScreen(
    navController: NavHostController,
    lifecycleOwner: LifecycleOwner,
    roomViewModel: RoomViewModel,
) {
    val username by roomViewModel.usernameState.observeAsState()
    val email by roomViewModel.emailState.observeAsState()

    LaunchedEffect(Unit) {
        roomViewModel.openDatabase() // before this my roomDatabase was closed
    }

    LaunchedEffect(roomViewModel.isOpen) {
        roomViewModel.fetchData()
    }
}

I was expecting to see in my logcat normal emails and usernames, maybe a list that I could use but all I got was null or empty strings. I have put Log statements everywhere to try to pin-point the problem. At first I thought that it was because that my database was closed in my database inspector, but I solved this and it is now open. I can see the dummy username and emails I have created but I still cannot access them. I have tried converting to flows or simple strings, but it did not work. I'm all out of ideas.


Solution

  • LiveData is not appropriate anymore when using Kotlin and Compose. You should switch to Flows.

    Dao:

    @Query("SELECT userName FROM info LIMIT 1")
    fun username(): Flow<String>
    
    @Query("SELECT email FROM info LIMIT 1")
    fun email(): Flow<String>
    

    Repository:

    fun username(): Flow<String> = db.dao.username()
    
    fun email(): Flow<String> = db.dao.email()
    

    I also adjusted the function names to be more aligned to Kotlin's naming conventions (don't use get, don't include type information in the name).

    The biggest change is in the view model. You do not need to open the database, that can be removed (_isOpen, isOpen, openDatabase, fetchData), from the repo as well. Also never expose suspend functions in the view model. upsertInfo works just fine as a regular, non-suspending function. Now replace the remaining properties with this:

    val usernameState: StateFlow<String?> = repo.username()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = null,
        )
    
    val emailState: StateFlow<String?> = repo.email()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = null,
        )
    

    That will convert the database flows to StateFlows. That is the modern replacement of LiveData: They also contain a value that can change over time and is observable.

    In your composables remove the LaunchedEffects. You can then simply convert the StateFlows to Compose State that is always updated when something changes in the database:

    val username by roomViewModel.usernameState.collectAsStateWithLifecycle()
    val email by roomViewModel.emailState.collectAsStateWithLifecycle()
    

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

    That's it: Everything should work as expected now.