0

I'm trying to collect callback flow from repository in viewmodel to save user Data. I have this two implementations:

CODE A)

private val _userState = MutableStateFlow<UserData?>(null)
val userState: StateFlow<UserData?> = _userState

init {
    getUserData()
}

private fun getUserData() {
    viewModelScope.launch {
        user?.uid?.let {
            userUseCases.getUserData(it).collect { response ->
                when (response) {
                    is Loading -> {}
                    is Success -> { _userState.value = response.data }
                    is Failure -> userNewMessage(response.e?.message ?: CANT_LOAD_DATA)
                }
                userState.value?.let { userData ->
                    if (!userData.hasProfileCompleted())
                        userNewMessage("Falta completar el perfil")
                }
            }
            
            Log.d("PROVES","LOG OUTSIDE COLLECT")
            
        }
    }
}

CODE B)

val userState2: StateFlow<UserData?> = flow {
    user?.uid?.let {
        userUseCases.getUserData(it).collect { dataResponse ->
            when (dataResponse) {
                is Loading -> Loading
                is Success -> emit(dataResponse.data)
                is Failure -> userNewMessage(dataResponse.e?.message ?: CANT_LOAD_DATA)
            }
        }
    }
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = UserData()
)

Two implementations are working but I don't know what approach is better, what one should I implement? Besides this, the Log in the first implementation outside the collect, never logs. Why is this happening? Is the function stuck in the collect?

Thanks!

3 Answers3

0

When you call collect(), it suspends the coroutine and doesn’t return until the Flow is complete. When your flow is monitoring something perpetually, it never completes. That’s why your log call in the first code example is never reached.

stateIn is obviously less convoluted code than the first way you’re doing it, but it also has the advantage of being able to use SharingStarted.WhileSubscribed so you aren’t wasting resources collecting the upstream flow when it’s not being used downstream.

However, your flow wrapper and inner collect call are convoluted. Here is a better way to write your code for stateIn:

val userState: StateFlow<UserData?> =
    (user?.uid?.let { userUseCases.getUserData(it)} ?: emptyFlow())
        .transform { dataResponse ->
            when (dataResponse) {
                is Loading -> Loading
                is Success -> emit(dataResponse.data)
                is Failure -> userNewMessage(dataResponse.e?.message ?: CANT_LOAD_DATA)
            }
        }
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = UserData()
    )

Although I’m a little confused about what you’re doing in the Loading condition where you simply declare the object Loading without doing anything with it.

I think this design would be better if you had a flow of some result sealed type that can be either Loading, Success, or Error.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • I tried your code but it gave me some errors and I didn't can test it. First userUseCases::getUserData error-> Type mismatch. Required: (TypeVariable(T)) → TypeVariable(R) Found: KProperty0 Then the .transform method is not working. Apart from this, in Loading condition I will update UIState with Loading. But in my code example it is not implemented. Thanks! – Alex Diaz Llagostera Apr 15 '23 at 16:53
  • Okey, solved. - > (user?.uid?.let { userUseCases.getUserData(it) } ?: emptyFlow()) Can you explain this? I think this design would be better if you had a flow of some result sealed type that can be either Loading, Success, or Error. I have a MutableStateFlow of my UI State, and Loading condition will update it. Is this correct? Thanks!! – Alex Diaz Llagostera Apr 15 '23 at 17:15
  • You're already using a sealed class or interface in your use case flow. I'm saying it would make sense to also do that for this view model flow instead of branching it out into a flow, some other error flow or something, and some other flow of whether it's loading (whatever you have planned for that). This flow is taking a simple combined flow and making it convoluted by sending the three possible result types down three different code paths that the UI layer will have to manage separately. – Tenfour04 Apr 15 '23 at 17:44
  • Can you show me an example of what are you talking? I'm not getting you. Thanks! – Alex Diaz Llagostera Apr 15 '23 at 21:40
  • Are you talking about resolve this flow in a composable directly? The thing is that this viewmodel will be a shared viewmodel for the profile feature and I'll need the userState in other screen. I don't know if you are talking about this. Thanks Tenfour! – Alex Diaz Llagostera Apr 15 '23 at 22:00
  • Maybe, or you might want to map it to something more specifically UI-related. Just seems convoluted to me that the UI layer will have to know about three different flows (or whatever you're using for Loading and errors) to manage one conceptual thing. – Tenfour04 Apr 16 '23 at 01:58
  • Okay, so maybe better approach is: val getUserData = user?.uid?.let { userUseCases.getUserData(it) } ?: emptyFlow() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = null ) Get it in a compose, and update UI State and UserData depending on this? Thanks Tenfour! – Alex Diaz Llagostera Apr 16 '23 at 10:27
0

Okay Finally I did this:

ViewModel)

val userDataResponse = (user?.uid?.let { userUseCases.getUserData(it) } ?: emptyFlow() )
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = Loading
    )

Composable)

when (val userDataResponse = userData2) {
        is Response.Loading -> {
            ProfileMainSignInButton(onSignInClick = onSignInClick )
        }
        is Response.Success -> {
            ProfileMainUserCard(
                userData = userDataResponse.data!!
            )

            ProfileMainSettingsMenu (
                onProfileSettingsClick = { onProfileSettingsClick() },
                onAccountSettingsClick = { onAccountSettingsClick() },
                onNotificationsSettingsClick = { onNotificationsSettingsClick() },
                onPrivacySettingsClick = { onPrivacySettingsClick() }
            )

            ProfileMainSignOutButton(onSignOutClick = { profileMainViewModel.onSignOutClicked() })
        }
        is Response.Failure -> {
            profileMainViewModel.userNewMessage(userDataResponse.e?.message ?: Constants.CANT_LOAD_DATA)
        }
    }
0

Here is another approach

private var _userDataResponse = MutableStateFlow<GetUserDataResponse>(Success(null))
val userDataResponse = _userDataResponse.asStateFlow()

init {
    viewModelScope.launch {
        user?.uid?.let { userId ->
            _userDataResponse.value = Loading
            userUseCases.getUserData(userId).collectLatest { response ->
                when(response) {
                    is Success -> { 
                        _profileMainUiState.update {  
                            it.copy(userData = response.data)
                        }
                        _userDataResponse.value = response
                    }
                    else -> _userDataResponse.value = response
                        
                }
                
            }
            checkProfile()
        }
    }
}
user16217248
  • 3,119
  • 19
  • 19
  • 37