Search code examples
android-jetpack-composeandroid-roomkotlin-flow

How can I retrieve the value from my roomDatabase before the Text composable needs it so it doesn't display null?


In my app, a Text composable calls the roomViewModel to display the energyState, but it shows null. I want the observer (the Text composable), to display the value of the "energy" column associated with the email the user inputs when the app starts. Here is my roomViewModel, which calls the repository, which calls the dao

class RoomViewModel(private val repository: Repository) : ViewModel() {

    var email = ""

    val energyState: StateFlow<Int?> = flowOf( email)
        .flatMapLatest {
            if (it == null) flowOf(null)
            else repository.getEnergy(it)
        }
        .stateIn(scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = null,
            )

    fun upsertInfo(info: Info) {
        viewModelScope.launch {
            repository.upsertInfo(info)
        }
    }
    fun updateEnergy(energy: Int, email: String) {
        viewModelScope.launch {
            repository.updateEnergy(energy, email)
            //energyState = energy // Update energyState directly
        }
    }

Here is the repository


class Repository(private val db: InfoDatabase) {

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

    suspend fun updateEnergy(energy: Int, email: String) {
        db.dao.updateEnergy(energy, email)
    }

    fun getEnergy(email: String): Flow<Int> {
        val energy = db.dao.getEnergy(email)
        return energy
    }

And here is the dao

@Query("SELECT energy FROM info WHERE email = :email LIMIT 1")
    fun getEnergy(email: String): Flow<Int>

I think that the energyState value is set to null because I don't have an email when the roomViewModel is initialized.

My signInViewModel is responsable to handle the logic with the signing in process when I get the email from the user by a login screen, a register screen or a previously logged user. It assigns the value of a firebaseAuth class currentUser email to the signinViewModel email mutableStateFlow variable and it keeps track of the authState of the firebase class.

class SignInViewModel(private val aut: FirebaseAuth): ViewModel(){
    
    private val _authState = MutableLiveData<AuthState>()
    val authState: LiveData<AuthState>  = _authState

    private val _email = MutableStateFlow<String?>("Lavoe")
    val email = _email.asStateFlow()
    
    init{
        checkAutStatus()
    }

    fun onSignInResult(result:SignInResult){
        _state.update{it.copy(
            isSignInSuccessful = result.data != null,
            signInError = result.errorMessage
        )}
    }
    fun checkAutStatus(){
        val currentUser = aut.currentUser
        if(currentUser == null) {
            _authState.value = AuthState.Unauthenticated
        }else{
            _email.value = currentUser.email
            _authState.value = AuthState.Authenticated
        }
    }
    fun login(email: String,password: String){
        if(email.isEmpty() || password.isEmpty()){
            _authState.value = AuthState.Error("Email or password can't be empty")
            return
        }
        _authState.value = AuthState.Loading
        aut.signInWithEmailAndPassword(email, password)
            .addOnCompleteListener{task ->
                if (task.isSuccessful){
                    _authState.value = AuthState.Authenticated
                }else{
                    _authState.value = AuthState.Error(task.exception?.message?:"Something went wrong")

                }
            }
    }

If my authentification state is "Authenticated" i assign to my roomViewModel the value of signInViewModel.email.value

LaunchedEffect(authState.value) {
        Log.i("tag", "authState in signInwithEmailScreen = ${authState}")
        when (authState.value){
            is AuthState.Authenticated -> {
                navController.navigate(route = "Intro_Screen")

                roomViewModel.email = signInViewModel.email.value ?: ""

If not, the user enters his email and presses a button, which receives the email from the outlineTextField, like this

Button(onClick = {
            roomViewModel.email = email  

My screen pattern goes like this MainActivity (roomDatabase initialized) -> ChooseSignInMethod -> LoginScreen (insert email) or RegisterScreen (insert email) or is already athenticated -> game screen (shows energyState value).

The problem I face is that I can't retrieve the energy value from my Room Database. It only shows null

It used to work great before I needed the email (I just showed the first user)

I tried to fiddle with the data types and different functions to retrieve the data but nothing worked.


Solution

  • It isn't quite clear what the individual view models actually contain, how they are related and what they need to do, so I will keep this answer more general.

    Usually you want to use view model properties to expose your repository flows as StateFlows. When a repository flow needs a parameter, like the email of your repository's getEnergy function, you have basically two options:

    1. Provide email during object creation of the view model. Then you can access it in the declaration of your StateFlow property:

      class MyViewModel(
          private val repository: Repository,
          email: String,
      ) : ViewModel() {
          val energyState: StateFlow<Int?> = repository.getEnergy(email)
              .stateIn(...)
      }
      

      This way the email is fixed and can never be changed for this view model instance.

    2. Keep email variable by storing it in a Flow itself. Then you can base your repository flow onto the email flow like this:

      class MyViewModel(
          private val repository: Repository,
      ) : ViewModel() {
          private val email = MutableStateFlow<String?>(null)
      
          val energyState: StateFlow<Int?> = email
              .flatMapLatest {
                  if (it == null) flowOf(null)
                  else repository.getEnergy(it)
              }
              .stateIn(...)
      }
      

      Whenever the email is changed the energyState is updated accordingly.

    Now, first you need to decide where the single source of truth for email should be. Then you need to decide how that should be provided to your various view models. And then you can use the above options to retrieve the energy flow for that email.