Using StateFlow To Update List Adapter - android

I am trying to switch from LiveData to StateFlow in populating my ListAdapter.
I currently have a MutableLiveData<List<CustomClass>> that I am observing to update the list adapter as such:
viewModel.mutableLiveDataList.observe(viewLifecycleOwner, Observer {
networkIngredientAdapter.submitList(it)
}
This works fine. Now I am replacing the MutableLiveData<List<CustomClass>?> with MutableStateFlow<List<CustomClass>?> in the viewModel as such:
private val _networkResultStateFlow = MutableStateFlow<List<IngredientDataClass>?>(null)
val networkResultStateFlow : StateFlow<List<IngredientDataClass>?>
get() = _networkResultStateFlow
fun loadCustomClassListByNetwork() {
viewModelScope.launch {
//a network request using Retrofit
val result = myApi.myService.getItems()
_networkResultStateFlow.value = result
}
}
I am collecting the new list in a fragment as such:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
}
However, the list Adapter does not update when I call loadCustomClassListByNetwork(). Why am I not able to collect the value

Try to replace code in fragment with below:
lifecycleScope.launch {
viewModel.networkResultStateFlow.flowWithLifecycle(lifecycle)
.collect { }
}

I haven't mentioned in the original question but there are two collect calls being made in the created coroutine scope as such:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
viewModel.listOfSavedIngredients.collectLatest(){
list -> localIngredientAdapter.submitList(list)}
}
Previously only the first collect call was working so that only one list was updating. So I just created two separate coroutine scopes as such and it now works:
lifecycleScope.launchWhenStarted {
viewModel.networkResultStateFlow.collect(){
list -> networkIngredientAdapter.submitList(list)}
}
lifecycleScope.launchWhenStarted {
viewModel.listOfSavedIngredients.collectLatest(){
list -> localIngredientAdapter.submitList(list)}
}
Note: Using launch, launchWhenStarted or launchWhenCreated yielded the same results.
I'll edit my response once I figure out the reason for needing separate scopes for each call to collect.
EDIT:
So the reason only one listAdapter was updating was because I needed a separate CoroutineScope for each of my StateFlow since Flows by definition run on coroutines. Each flow uses its respective coroutine scope to collect its own value and so you cannot have flows share the same coroutine scope b/c then they would be redundantly collecting the same value. The answer provided by #ruby6221 also creates a new coroutine scope and so likely works but I cannot test it due to an unrelated issue with upgrading my SDK version, otherwise I would set it as the correct answer.

Related

How to write Android ViewModel properly and move logic out of it?

I'm trying to use MVVM with ViewModel, ViewBinding, Retrofit2 in Android/Kotlin.
I don't know how to move application logic from ViewModel. I can't just move methods with logic because they run on viewModelScope and put results into observable objects in my ViewModel.
Or maybe I can?
For example I have some ArrayList (to show on some ListView).
// items ArrayList
private val _stocktakings =
MutableLiveData<ArrayList<InventoryStocktakingWithCountsDto?>>(ArrayList())
val stocktakings : LiveData<ArrayList<InventoryStocktakingWithCountsDto?>> get() =
_stocktakings
// selected item
private val _selected_stocktaking = MutableLiveData<Int>>
val selected_stocktaking : LiveData<Int> get() = _selected_stocktaking
And a function that is called from my fragment:
public fun loadStocktakings() {
viewModelScope.launch {
Log.d(TAG, "Load stocktakings requested")
clearError()
try {
with(ApiResponse(ApiAdapter.apiClient.findAllStocktakings())){
if (isSuccessful && body != null){
Log.d(TAG, "Load Stocktakings done")
setStocktakings(body)
} else {
val e = "Can't load stocktakings, API error: {${errorMessage}}"
Log.e(TAG, e)
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
} catch (t : Throwable) {
Log.e(TAG, "Can't load stocktakings, connectivity error: ${t.message}")
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
}
Now I want to add another function that changes some field in one of stocktakings. Maybe something like:
public fun setSelectedStocktakingComplete() {
stocktakings.value[selected_stocktaking.value].isComplete = true;
// call some API function... another 15 lines of code?
}
How to do it properly?
I feel I have read wrong tutorials... This will end with fat viewmodel cluttered with viewModelScope.launch, error handling and I can't imagine what will happen when I start adding data/form validation...
Here, some tip for that
Make sure the ViewModel is only responsible for holding and managing
UI-related data.
Avoid putting business logic in the ViewModel. Instead, encapsulate
it in separate classes, such as Repository or Interactor classes.
Use LiveData to observe data changes in the ViewModel and update the
UI accordingly.
Avoid making network or database calls in the ViewModel. Instead,
use the Repository pattern to manage data operations and provide the
data to the ViewModel through a LiveData or other observable object.
Make sure the ViewModel does not hold context references, such as
Activity or Fragment.
Use a ViewModel factory to provide dependencies to the ViewModel, if
necessary.
you can ensure that your ViewModel is simple, easy to test,
and scalable. It also makes it easier to maintain your codebase, as
the business logic is separated from the UI logic.
hope you understand

Update RecyclerView from StateFlow doesn't work

Is there a way to update (automatically) the RecyclerView when a list is populated with data?
I created a simple app (here is the repository for the app).
In HomeFragment there is a RecyclerView and a button to refresh the data.
The app works fine as long as I have the following code in HomeFragment to update the adapter whenever the StateFlow list gets data.
private fun setupObservers() {
lifecycleScope.launchWhenStarted {
vm.state.collect() {
if (it.list.isNotEmpty()) {
todoAdapter.data = it.list
} else {
todoAdapter.data = emptyList()
}
}
}
}
My question is, is there a away for the RecyclerView to update, without having to observe (or collect) the changes of the list of the StateFlow?
Something has to notify the RecyclerView adapter when the data has changed. Either you do it in a collector/observer, or you have to proactively do it in every place in your code where you do something that might affect the data. So, it is much easier and less error-prone to do it by collecting.
Side note, the if/else in your code doesn't accomplish anything useful. No reason to treat an empty list differently if you still end up passing an empty list to the adapter.
It's more correct to use repeatOnLifecycle (or flowWithLifecycle) than launchWhenStarted. See here.
private fun setupObservers() {
vm.state.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.onEach { todoAdapter.data = it.list }
.launchIn(viewLifecycleOwner.lifecycleScope)
}
I personally like to use an extension function like this to make it more concise wherever I'm collecting flows:
fun <T> Flow<T>.launchAndCollectWithLifecycle(
lifecycleOwner: LifecycleOwner,
state: Lifecycle.State = Lifecycle.State.STARTED,
action: suspend (T) -> Unit
) = flowWithLifecycle(lifecycleOwner.lifecycle, state)
.onEach(action)
.launchIn(lifecycleOwner.lifecycleScope)
Then your code would become:
private fun setupObservers() {
vm.state.launchAndCollectWithLifecycle(viewLifecycleOwner) {
todoAdapter.data = it.list
}
}

ViewModel + Room test coverage in UnitTest

I have an unit test like this:
...
subj.mintToken(to, value, uri)
advanceUntilIdle()
...
val pendingTxFinalState = subj.uiState.value.pendingTx.count()
assertThat("Model should have a single pending tx, but has $pendingTxFinalState", pendingTxFinalState == 1)
...
The model field in ViewModel is populated by the request to cache in the init {} block. Each change in table would trigger this coroutine flow. This piece of unit test checks correctness of this functionality.
The current issue is this Flow in init {} block is triggered only on the test start when ViewModel instance is created. It does not respond on update in table.
It is important to note I don't use in test a room database neither test database, but FakeCacheRepository where behaviour of methods are emulated by flow with mocked data. However the behaviour of flow should be the same as there is still in change in underlying data.
val txPool = ConcurrentLinkedQueue<ITransaction>()
override fun createChainTx(tx: ITransaction): Flow<ITransaction> {
return flow {
txPool.add(tx)
emit(tx)
}
}
override fun getAllChainTransactions(): Flow<List<ITransaction>> {
return flow {
emit(txPool.toList())
}
}
Do you see the issue here or better way to test this?
My guess is you’re writing you’re own FakeCacheRepo and in the update function you are calling createChainTx. The value of the flow isn’t updating though because the create function doesn’t just update the value it creates a new flow instead of updating the old one. You can modify the set up to emit continuously in a loop (with some buffer delay) based on a variable. Then when you change the variable it will change what the current flow is emiting as expected.
The code example here is roughly doing that: https://developer.android.com/kotlin/flow#create
override fun createChainTx(): Flow<ITransaction> {
return flow {
while(true) {
val tx = getLatestTxValue() // Get the latest updated value from an outside source
txPool.add(tx)
emit(tx)
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}

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()
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"
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

Is livedata builder ok for one-shot operations?

For example, let's say that we have a product catalog view with an option to add product to a cart.
Each time when user clicks add to cart, a viewModel method addToCart is called, that could look like this:
//inside viewModel
fun addToCart(item:Item): LiveData<Result> = liveData {
val result = repository.addToCart(item) // loadUser is a suspend function.
emit(result)
}
//inside view
addButton.onClickListener = {
viewModel.addToCart(selectedItem).observe (viewLifecycleOwner, Observer () {
result -> //show result
}
}
What happens after adding for example, 5 items -> will there be 5 livedata objects in memory observed by the view?
If yes, when will they be cleanup? And if yes, should we avoid livedata builder for one-shot operations that can be called multiple times?
Your implementation seems wrong! You are constantly returning a new LiveData object for every addToCard function call. About your first question, it's a Yes.
If you want to do it correctly via liveData.
// In ViewModel
private val _result = MutableLiveData<Result>()
val result: LiveData<Result>
get() = _result;
fun addToCart(item: Item) {
viewModelScope.launch {
// Call suspend functions
result.value = ...
}
}
// Activity/Fragment
viewModel.result.observe(lifecycleOwner) { result ->
// Process the result
...
}
viewModel.addToCart(selectedItem)
All you have to do is call it from activity & process the result. You can also use StateFlow for this purpose. It also has an extension asLiveData which converts Flow -> LiveData as well.
According to LiveData implementation of:
public void observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
a new Observer (wrapper) is added every time you observe a LiveData. Looking at this I would be carefull creating new Observers from a view (click) event. At the moment I can not tell if a Garbage Collector can free this resources.
As #kaustubhpatange mentioned, you should have one LiveData with a state/value that can be changed by the viewModel, with every new result. That LiveData can be observed (once) in your Activity or Fragment onCreate() function:
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.result.observe(lifecycleOwner) { result ->
// handle the result
}
}
Using MutableLiveData in your ViewModel, you can mostly create LiveData only once, and populate it later with values from click events, responses etc.
TL;DR
If your operation is One-Shot use Coroutine and LiveData.
If your operation serving with Streams you can use Flow.
For one-shot operations, your approach it's OK.
I think with liveData builder there is no any Memory leak.
If you use for example private backing property for LiveData and observe an public LiveData it might occurs different behavior like get latest value before assign new value to that.

Categories

Resources