I'm using Flow to get data from Room then I call Flow.collect in my FragmentLogic class. Inside collect{} I do some work and update the View through an Interface( example: view.updateAdapter(collectData)). This works fine until an onConfigurationChanged is called and the screen rotates, the code inside collect executes and works in Logcat but anything that changes the UI does not work, the function updateAdapter() is called however nothing happens. One solution I found is to call the function beginObservingProductList() again in onStart() if savedinstance is not null but that creates two instances of the collect{} and they both show in logcat.
I want the UI changes to work even after onConfigurationChanged is called.
Room Dao class:
#Query("SELECT * FROM product")
fun observableList(): Flow<List<ProductEntity>>
then in the implementation:
productDao.observableList().map { collection ->
collection.map { entity ->
entity.toProduct
}
}.flowOn(DispatchThread.io())
And finally I collect the data and change the view:
private fun beginObservingProductList() = this.launch {
vModel.liveProductList.map {
mapToSelectionProduct(it)
}.collect {
ui { view.updateAdapter(it) }
if (it.isNotEmpty()) {
filledListState()
} else {
emptyListState()
}
updateCosts()
vModel.firstTime = false
}
}
Flow is not lifecycle aware, you should use LiveData to handle configuration changes.
To use LiveData with Flow,
implement androidx.lifecycle:lifecycle-livedata-ktx:2.2.0, then you can use .asLiveData() extension func.
Repository
fun getList()= productDao.observableList().map { collection ->
collection.map { entity ->
entity.toProduct
}
}.flowOn(DispatchThread.io())
ViewModel
val liveData = repository.getList().asLiveData()
Activity/Fragment
viewModel.liveData.observe(this,Observer{ list->
//do your thing
})
Related
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
}
}
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.
Scenario
Hi,
I have an Activity with a ViewPager. In the ViewPagerAdapter, I create instances of a same fragment with different data.
And in each instance I initialize a ViewModel
val dataViewModelFactory = this.activity?.let { DataViewModelFactory(it) }
mainViewModel = ViewModelProviders.of(this, dataViewModelFactory).get(MainViewModel::class.java)
In my fragment, I observe two MutableLiveData when I call APIs
mainViewModel.isResponseSuccessful.observe(this, Observer { it ->
if(it) {
//do Something
}else{
Toast.makeText(activity, "Error in Sending Request", Toast.LENGTH_SHORT).show()
}
})
mainViewModel.isLoading.observe(this, Observer {
if (it) {
println("show progress")
} else {
println("dismiss progress")
}
})
In each fragment, on a button click I load another fragment. And if required call and API to fetch data.
PROBLEM
The code comes to the observe block multiple times in my fragment. When I comeback from one fragment to previous fragment, even though no API is called, the code on observe block is executed.
What I tried
I tried using an activity instance in the ViewModel initialization
mainViewModel = ViewModelProviders.of(activity,dataViewModelFactory).get(MainViewModel::class.java)
But it did not work.
Please help,
If you want to prevent multiple calls of your observer u can just change MutableLiveData to SingleLiveEvent. Read this
It might help you:
import java.util.concurrent.atomic.AtomicBoolean
class OneTimeEvent<T>(
private val value: T
) {
private val isConsumed = AtomicBoolean(false)
private fun getValue(): T? =
if (isConsumed.compareAndSet(false, true)) value
else null
fun consume(block: (T) -> Unit): T? =
getValue()?.also(block)
}
fun <T> T.toOneTimeEvent() =
OneTimeEvent(this)
First, when you want to post a value on LiveData, use toOneTimeEvent() extension function to wrap it in a OneTimeEvent:
liveData.postValue(yourObject.toOneTimeEvent())
Second, when you are observing on the LiveData, use consume { } function on the delivered value to gain the feature of OneTimeEvent. You'll be sure that the block of consume { } will be executed only once.
viewModel.liveData.observe(this, Observer {
it.consume { yourObject ->
// TODO: do whatever with 'yourObject'
}
})
In this case, when the fragment resumes, your block of code does not execute again.
Given the following components
data class Account(val name: String)
data class GetAccountRequest(val name: String)
#Dao
interface AccountDao {
#Query("SELECT * FROM accounts ORDER BY name ASC")
fun all(): LiveData<List<Account>>
}
interface AccountOperations {
#GET("/foo/account")
suspend fun getAccount(#Body request: GetAccountRequest): Account
}
class AccountRepository(private val dao: AccountDao, private val api: AccountOperations) {
val accounts: LiveData<List<Account>> = dao.all()
suspend fun refresh(name: String) {
val account = api.getAccount(GetAccountRequest(name))
dao.insert(account)
}
}
I am working on an Android application that is using these components (powered by Room for the database and Retrofit for API access).
In my ViewModel I maintain a RecyclerView that lists all accounts. I enable users to refresh that list manually. The respective (part of the) ViewModel looks like this:
fun refresh() {
viewModelScope.launch {
repository.accounts.value?.forEach {
launch { repository.refresh(it.name) }
}
}
Timber.i("Done refreshing!")
}
I do want the refresh to update all accounts in parallel, this is why I am using launch. I have also decided to do this in the ViewModel, rather than in the repository, since that would have required to launch a new coroutine in the repository. Which per this post is discouraged since repositories don't have a natural lifecycle.
The above function, refresh, is invoked from the UI and shows a refresh-indicator while the RecyclerView is updated. So I want to stop this indicator once all accounts have been updated.
My code as shown above doesn't do this, since it will launch all the updates and then print the log statement before all updates have been finished. As a result the refresh-indicator disappears although there are still updates.
So my question (finally) is: how can I refactor the code so that it runs all updates in parallel, but makes sure refresh doesn't return before all of them have finished?
EDIT #1
Going back to what I want to achieve: showing the refresh-indicator while the view is updating, I came up with the following (changed the refresh function in the ViewModel):
fun refresh() {
viewModelScope.launch {
try {
coroutineScope {
_refreshing.value = true
repository.accounts.value?.map { account ->
async {
repository.refresh(account.name)
}
}
}
} catch (cause: CancellationException) {
throw cause
} catch (cause: Exception) {
Timber.e(cause)
} finally {
_refreshing.value = false
}
}
}
The ViewModel exposes a LiveData for when it is refreshing and the fragment can observe it to show or hide the spinner. This seems to do the trick. However, it still doesn't feel right and I appreciate any improved solutions.
In order to await for all of your parallel refresh() operations, simply use awaitAll():
coroutineScope.launch {
_refreshing.value = true
repository.accounts.value?.map { account ->
async {
repository.refresh(account.name)
}
}.awaitAll()
_refreshing.value = false
}
Furthermore, It's not advised to wrap coroutines with try/catch.
You can read more on this here.
I have my architecture like so:
Dao methods returning Flow<T>:
#Query("SELECT * FROM table WHERE id = :id")
fun itemById(id: Int): Flow<Item>
Repository layer returning items from DB but also backfilling from network:
(* Need help here -- this is not working as intended **)
fun items(): Flow<Item> = flow {
// Immediately emit values from DB
emitAll(itemDao.itemById(1))
// Backfill DB via network request without blocking coroutine
itemApi.makeRequest()
.also { insert(it) }
}
ViewModel layer taking the flow, applying any transformations, and converting it into a LiveData using .asLiveData():
fun observeItem(): LiveData<Item> = itemRepository.getItemFlow()
.map { // apply transformation to view model }
.asLiveData()
Fragment observing LiveData emissions and updating UI:
viewModel.item().observeNotNull(viewLifecycleOwner) {
renderUI(it)
}
The issue I'm having is at step 2. I can't seem to figure out a way to structure the logic so that I can emit the items from Flow immediately, but also perform the network fetch without waiting.
Since the fetch from network logic is in the same suspend function it'll wait for the network request to finish before emitting the results downstream. But I just want to fire that request independently since I'm not interested in waiting for a result (when it comes back, it'll update Room and I'll get the results naturally).
Any thoughts?
EDIT
Marko's solution works well for me, but I did attempt a similar approach like so:
suspend fun items(): Flow<List<Cryptocurrency>> = coroutineScope {
launch {
itemApi.makeRequest().also { insert(it) }
}
itemDao.itemById(1)
}
It sounds like you're describing a background task that you want to launch. For that you need access to your coroutine scope, so items() should be an extension function on CoroutineScope:
fun CoroutineScope.items(): Flow<Item> {
launch {
itemApi.makeRequest().also { insert(it) }
}
return flow {
emitAll(itemDao.itemById(1))
}
}
On the other hand, if you'd like to start a remote fetch whose result will also become a part of the response, you can do it as follows:
fun items(): Flow<Item> = flow {
coroutineScope {
val lateItem = async { itemApi.makeRequest().also { insert(it) } }
emitAll(itemDao.itemById(1))
emit(lateItem.await())
}
}