Abhishek AN
Abhishek AN

Reputation: 808

How to combine livedata and kotlin flow

Is this good to put the collect latest inside observe?

viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
            .observe(viewLifecycleOwner) {
                if (it != null) {
                    viewLifecycleOwner.lifecycleScope.launch {
                        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                            launch {
                                viewModel.referralDetailsResponse.collect { referralResponseState ->
                                    when (referralResponseState) {
                                        State.Empty -> {
                                        }
                                        is State.Failed -> {
                                            Timber.e("${referralResponseState.message}")
                                        }
                                        State.Loading -> {
                                            Timber.i("LOADING")
                                        }
                                        is State.Success<*> -> {
                                            // ACCESS LIVEDATA RESULT HERE??
}}}}

I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?

My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response

private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()

init {
        val inviteSlug: String? = savedStateHandle["inviteSlug"]
        // Fire invite link
        if (inviteSlug != null) {
            referralDetail(inviteSlug)
        }
    }

fun referralDetail(referral: String?) = viewModelScope.launch {
        _referralDetailsResponse.send(State.Loading)
        when (
            val response =
                groupsRepositoryImpl.referralDetails(referral)
        ) {
            is ResultWrapper.GenericError -> {
                _referralDetailsResponse.send(State.Failed(response.error?.error))
            }
            ResultWrapper.NetworkError -> {
                _referralDetailsResponse.send(State.Failed("Network Error"))
            }
            is ResultWrapper.Success<*> -> {
                _referralDetailsResponse.send(State.Success(response.value))
            }
        }
    }

fun fetchUserProfileLocal(username: String) =
        userRepository.getUserLocal(username).asLiveData()

Upvotes: 3

Views: 3152

Answers (2)

Sergio
Sergio

Reputation: 30655

You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:

combine(
  viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
  viewModel.referralDetailsResponse
) { userProfile, referralResponseState  ->
  ...
}.launchIn(viewLifecycleOwner.lifecycleScope)

But it is better to move combining logic to ViewModel class and observe the overall result.

Dependency to use LiveData.asFlow() extension function:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"

Upvotes: 6

Klitos G.
Klitos G.

Reputation: 836

it certainly is not a good practice to put a collect inside the observe.

I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata

for example in your first code block I would change it like this

  • get rid of "userProfile" from your viewmodel
  • create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
  • then in your viewmodel, where you would update the "userProfile" update the three new state objects instead

this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel

For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:

private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()

What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again

Upvotes: 0

Related Questions