How to replace LiveData with Flow
Asked Answered
F

2

4

I've one LiveData named sortOrder and then I've another variable named myData that observes any change to sortOrder and populates data accordingly.

class TestViewModel @ViewModelInject constructor() : ViewModel() {

    private val sortOrder = MutableLiveData<String>()

    val myData = sortOrder.map {
        Timber.d("Sort order changed to $it")
        "Sort order is $it"
    }

    init {
        sortOrder.value = "year"
    }

}

Observing in Activity

class TestActivity : AppCompatActivity() {

    private val viewModel: TestViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        
        // Observing data
        viewModel.myData.observe(this) {
            Timber.d("Data is : $it")
        }
    }
}

Question

  • How can I replace the above scenario with Flow/StateFlow APIs without any change in output?
Floppy answered 9/1, 2021 at 14:11 Comment(5)
Would mapping Flow to LiveData using asLiveData be an option for you?Mycostatin
Yes, I can do that in the activity. The problem is if I use StateFlow for sortOrder, the map body get triggered every time the activity recreated.Floppy
@Floppy Why not just use livedata transforms in the viewmodel and exposing flow as livedata to the view?Borisborja
@Borisborja The whole point is to make the VM liveData free.Floppy
No solution is behaviorally correct unless it relies on SavedStateHandle, but the only safe way to get SavedStateHandle change notifications is with SavedStateHandle.getLiveData. If you want to keep it LiveData-free, don't use ViewModel.Kittykitwe
I
4

If you fail to convert the mapped cold Flow into a hot Flow, it will restart the flow every time you collect it (like when your Activity is recreated). That's how cold flows work.

I have a feeling they will flesh out the transform functions for StateFlow/SharedFlow, because it feels very awkward to map them to cold flows and have to turn them back into hot flows.

The public Flow has to be a SharedFlow if you don't want to manually map the first element distinctly because the stateIn function requires you to directly provide an initial state.

    private val sortOrder = MutableStateFlow("year")

    val myData = sortOrder.map {
        Timber.d("Sort order changed to $it")
        "Sort order is $it"
    }.shareIn(viewModelScope, SharingStarted.Eagerly, 1)

Or you could create a separate function that is called within map and also in a stateIn function call.

    private val sortOrder = MutableSharedFlow<String>()
    
    private fun convertSortOrder(order: String): String {
        Log.d("ViewModel", "Sort order changed to $order")
        return "Sort order is $order"
    }

    val myData = sortOrder.map {
        convertSortOrder(it)
    }.stateIn(viewModelScope, SharingStarted.Eagerly, convertSortOrder("year"))
Island answered 9/1, 2021 at 18:13 Comment(3)
Wow. It works. Thank you so much for the detailed answer. I really appreciate it.Floppy
Any specific reason to use SharingStarted.Eagerly? Why not SharingStarted.Lazily?Floppy
I don't think it makes a difference in this case, because you are collecting it immediately after the ViewModel is instantiated anyway. If it were a something that you only sometimes collect if the user does something, then you would be deciding between avoiding unnecessary processing by making it lazy or prefetching to make your app faster by making it eager.Island
M
0

From the point of the fragment/activity, you have to create a job that collects the flow in onStart() and cancel it in onStop(). Using the lifecycleScope.launchWhenStarted will keep the flow active even in the background.

Use the bindin library to migrate to Flow with ease. I'm biased, tho.

See an article on medium about it.

Merrifield answered 13/3, 2021 at 12:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.