I have a recycler view with fixed number widgets vertically in a specific order. Some of the widgets also contain tabular data hence I've considered using nested recycler view also within it.
Every widget makes http call asynchronously from the ViewModel and binds the data to the epoxyController as I mentioned below.
As requestModelBuild() being called for every widget as they receive the data through the public setters for example priceViewData, packageData and etc from where requestModelBuild() is called. So in this instance every widget bind happens regardless of every time when data is received for any of the widgets.
This seems to be expensive also, there some analytics gets fired as we needed for every bind.
So, here the analytics call for the widget is multiplied.
Please suggest if this can be handled through the epoxy without handling manually.
class ProductDetailsEpoxyController(val view: View?,
private val name: String?,
private val context: Context?) :
AsyncEpoxyController() {
private val args = bundleOf("name" to name)
var priceViewData: IndicativePriceViewData? = emptyPriceViewData()
set(value) {
field = value
requestModelBuild()
}
var packageData: PackageViewData? = emptyPackageWidgetViewData()
set(value) {
field = value
requestModelBuild()
}
...
...
override fun buildModels() {
buildPriceViewData()
buildPackageViewData()
....
}
private fun buildPriceViewData(){
priceViewData?.let {
id("price")
priceViewDataModel(it)
}
}
private fun buildPackageViewData(){
packageViewData?.let {
id("package")
packageViewDataModel(it)
}
}
...
...
}
From Epoxy's Wiki:
Adapter and diffing details
Once models are built, Epoxy sets the new models on the backing adapter and runs a diffing algorithm to compute changes against the previous model list. Any item changes are notified to the RecyclerView so that views can be removed, inserted, moved, or updated as necessary.
So basicallly, this ensures not all models will be updated.
The issue that you're facing is possibly related to:
Using DataBinding
Your classes are not implemented equals and hashCode the way you want.
The problem with using Objects in DataBinding is that, every time the object is updated, all fields that depend on the object are also updated, even if not all changed.
If your classes are normal classes and not data classes or you expect a different behavior when executing priceData1 == priceData2 (for example, only comparing the data's id), you should override this methods so Epoxy detect changes correctly. Also, you can use DoNotHash option for EpoxyAttribute so the class is not added to the model's hashCode function. More info
Related
I have a BaseViewModel that basically has the function to get the user data like so:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun getUserData() = viewModelScope.launch {
_userResponse.value = Resource.Loading
_userResponse.value = repository.getLoggedInUserData()
}
}
In my Fragment, I access this data by just calling viewModel.getUserData(). This works. However, I'd like to now be able to edit the data. For example, the data class of UserResponse looks like this:
data class UserResponse(
var id: Int,
var username: String,
var email: String
)
In other fragments, I'd like to edit username and email for example. How do I do access the UserResponse object and edit it? Is this a good way of doing things? The getUserData should be accessed everywhere and that is why I'm including it in the abstract BaseViewModel. Whenever the UserResponse is null, I do the following check:
if (viewModel.userResponse.value == null) {
viewModel.getUserData()
}
If you want to be able to edit the data in userResponse, really what you're talking about is changing the value it holds, right? The best way to do that is through the ViewModel itself:
abstract class BaseViewModel(
private val repository: BaseRepository
) : ViewModel() {
private var _userResponse: MutableLiveData<Resource<UserResponse>> = MutableLiveData()
val userResponse: LiveData<Resource<UserResponse>> get() = _userResponse
fun setUserResponse(response: UserResponse) {
_userResponse.value = response
}
...
}
This has a few advantages - first, the view model is responsible for holding and managing the data, and provides an interface for reading, observing, and updating it. Rather than having lots of places where the data is manipulated, those places just call this one function instead. That makes it a lot easier to change things later, if you need to - the code that calls the function might not need to change at all!
This also means that you can expand the update logic more easily, since it's all centralised in the VM. Need to write the new value to a SavedStateHandle, so it's not lost if the app goes to the background? Just throw that in the update function. Maybe persist it to a database? Throw that in. None of the callers need to know what's happening in there
The other advantage is you're actually setting a new value on the LiveData, which means your update behaviour is consistent and predictable. If the user response changes (either a whole new one, or a change to the current one) then everything observeing that LiveData sees the update, and can decide what to do with it. It's less brittle than this idea that one change to the current response is "new" and another change is "an update" and observers will only care about one of those and don't need to be notified of the other. Consistency in how changes are handled will avoid bugs being introduced later, and just make it easier to reason about what's going on
There's nothing stopping you from updating the properties of the object held in userResponse, just like there's nothing stopping you from holding a List in a LiveData, and adding elements to that list. Everything with a reference to that object will see the new data, but only if they look at it. The point of LiveData and the observer pattern is to push updates to observers, so they can react to changes (like, say, updating text displayed in a UI). If you change one of the vars in that data class, how are you going to make sure everything that needs to see those changes definitely sees them? How can you ensure that will always happen, as the app gets developed, possibly by other people? The observer pattern is about simplifying that logic - update happens, observers are notified, the end
If you are going to do things this way, then I'd still recommend putting an update function in your VM, and let that update the vars. You get the same benefits - centralising the logic, enabling things like persistence if it ever becomes necessary, etc. It could be as simple as
fun setUserResponse(response: UserResponse) {
_userResponse.value?.run {
id = response.id
username = response.username
email = response.email
}
}
and if you do decide to go with the full observer pattern for all changes later, everything is already calling the function the right way, no need for changes there. Or you could just make separate updateEmail(email: String) etc functions, whatever you want to do. But putting all that logic in the VM is a good idea, it's kinda what it's there for
Oh and you access that object through userResponse.value if you want to poke at it - but like I said, better to do that inside a function in the VM, keep that implementation detail, null-safety etc in one place, so callers don't need to mess with it
The ideal way to update userResponse you should change/edit _userResponse so that your userResponse we'll give you the updated data.
it should be something like this
_userResponse.value = Resource<UserResponse>()
In my app i use the Room library to handle user data, all the functionality has been implemented like in the "Android Basics in Kotlin" Tutorial Unit 5 on developer.android.com.
In one fragment i need to fetch a single item out of the database - for that i implemented the function in the fragment's viewmodel:
fun retrievePlaceItem(id: Int): LiveData<PlaceItem> {
return itemDao.getPlaceItem(id).asLiveData()
}
the ItemDao is passed into the ViewModel Factory from the Room Database instance, which itself is instantiated in the custom Application class.
this is the query used in the ItemDao interface:
#Query("SELECT * FROM placeItem WHERE id = :id")
fun getPlaceItem(id: Int): Flow<PlaceItem>
Data in the ItemDao is returned as a Flow, and turned into LiveData in the fetching function.
The Fragment itself observes the return of the function with a passed id, and when the observer triggers, the value is stored in a lateinit var of the corresponding datatype.
lateinit var placeItem: PlaceItem
...
override fun onViewCreated(...) {
super.onViewCreated(view, savedInstanceState)
...
val id = navigationArgs.itemId
sharedViewModel.retrievePlaceItem(id)
.observe(this.viewLifecycleOwner) { selectedItem ->
placeItem = selectedItem
}
...
}
this works flawlessly, the item is retrieved, the observer gets triggered, and the lateinit var placeItem is initialized for further use.
in another fragment, that follows later on, i use a different viewmodel with the exact same function - i try to retrieve the value in the exact same way, observing the function return within the onViewCreated method of the fragment. the code is exactly the same, and i tried comparing it to the things taught in the tutorial - no deviations whatsoever. when i now go to use the value, i get an error
kotlin.UninitializedPropertyAccessException: lateinit property placeItem has not been initialized
after inspecting my code using logs, i understood the following:
the viewmodel function to retrieve the item is called
the correct item id is used
the code inside the observer curly brackets is not executed
i tried using the same viewmodel in both fragments, anything until there was no more conceivable difference between these two pieces of code. yet the first one works, the second one doesn't. something in my code creates a difference between the two instances of me using the database to fetch an item.
I avoided the problem by moving the other functions, that will handle the lateinit var, into the observer brackets. i'm unsure as to why that was not needed in my other fragment, but this works just fine!
I know that the Paging3 library was designed to work together with RecyclerView, however I have a use case where the paged results are also presented on a map. If you look inside the PagingDataAdapter class, you will notice that it is backed by AsyncPagingDiffer. So for now, I'm trying to make it work using the AsyncPagingDiffer class, which in turn receives a ListUpdateCallback, so that UI is notified when data updates occur. Thus, as soon as ListUpdateCallback dispatches any update, I should be able to retrieve the data just by calling AsyncPagingDiffer.snapshot().
This snippet illustrates well what I'm trying to do:
class MapAdapter : ListUpdateCallback {
private val differ = AsyncPagingDataDiffer(MapDiff(), this)
suspend fun submitData(pagingData: PagingData<Foo>) {
differ.submitData(pagingData)
}
override fun onInserted(position: Int, count: Int) {
val data = differ.snapshot()
// Update UI
}
// Other callbacks...
}
but the snapshot is always empty or out of date when trying to recover it this way. In other words, the snapshot is actually available only after the callback has already been notified, which to me is unwanted behavior.
I can confirm that this approach works with Paging 2 (or whatever it is called), but I wish there was some way to use it with Paging 3 as well, as I am reluctant to downgrade other features that are underway with Paging 3.
So I have a RecyclerView with infinite scrolling. I first do a network call to my API and get a first page of 20 items.
In my ViewModel (code below), I have an observable that triggers the network call in my repository using the page number.
So, when the user scrolls to the bottom, the page number is incremented, and it triggers another network request.
Here's the code in my ViewModel:
private val scheduleObservable = Transformations.switchMap(scheduleParams) { params: Map<String, Any> ->
ScheduleRepository.schedule(params["organizationId"] as String, params["page"] as Int)
}
// This is the method I call in my Fragment to fetch another page
fun fetchSchedule(organizationId: String, page: Int) {
val params = mapOf(
"organizationId" to organizationId,
"page" to page
)
scheduleParams.value = params
}
fun scheduleObservable() : LiveData<Resource<Items>> {
return scheduleObservable
}
In my fragment, I observe scheduleObservable, and when it emits data, I append them to my RecyclerView's adapter:
viewModel.scheduleObservable().observe(this, Observer {
it?.data?.let {
if (!isAppending) {
adapter.replaceData(it)
} else {
adapter.addData(it)
}
}
})
The problem with my current implementation is that, on configuration change, I rebind my observer, and the observable emits the last fetched data. In my case, it will emit the last fetched page only.
When coming back from a configuration change, I would want to have the full list of items fetched to this point so I can repopulate the adapter with these.
I'm wondering what's the best way to solve this. Should I have two observables? Should I create a list variable in my ViewModel to store all the items fetched and use that list for my adapter?
I checked android-architecture-components on GitHub, but it's usually overkill compared for my needs (no database, no paging library, etc) and I get lost since I am still trying to wrap my head around architecture components.
I have been implementing the new Paging Library with a RecyclerView with an app built on top of the Architecture Components.
The data to fill the list is obtained from the Room database. In fact, it is fetched from the network, stored on the local database and provided to the list.
In order to provide the necessary data to build the list, I have implemented my own custom PageKeyedDataSource. Everything works as expected except for one little detail. Once the list is displayed, if any change occurs to the data of a list's row element, it is not automatically updated. So, if for example my list is showing a list of items which have a field name, and suddenly, this field is updated in the local Room database for a certain row item, the list does not update the row UI automatically.
This behaviour only happens when using a custom DataSource unlike when the DataSource is obtained automatically from the DAO, by returning a DataSource Factory directly. However, I need to implement a custom DataSource.
I know it could be updated by calling the invalidate() method on the DataSource to rebuild the updated list. However, if the app is showing 2 lists at a time (half screen each for example), and this item appears in both lists, it would be needed to call invalidate() for both lists separately.
I have thought with a solution in which, instead of using an instance of the item's class to fill each ViewHolder, it uses a LiveData wrapped version of it, to make each row observe for changes on its own item and update that row UI when necessary. Nevertheless, I see some downsides on this approach:
A LifeCycleOwner (such as the Fragment containing the RecyclerView for example) must be passed to the PagedListAdapter and then forward it to the ViewHolder in order to observe the LiveData wrapped item.
A new observer will be registered for each list's new row, so I do not know at all if it has an excessive computational and memory cost, considering it would be done for every list in the app, which has a lot of lists in it.
As the LifeCycleOwner observing the LiveData wrapped item would be, for example, the Fragment containing the RecyclerView, instead of the ViewHolder itself, the observer will be notified every time a change on that item occurs, even if the row containing that item is not even visible at that moment because the list has been scrolled, which seems to me like a waste of resources that could increase the computational cost unnecessarily.
I do not know at all if, even considering those downsides, it could seem like a decent approach or, maybe, if any of you know any other cleaner and better way to manage it.
Thank you in advance.
Quite some time since last checked this question, but for anyone interested, here is the cause of my issue + a library I made to observe LiveData properly from a ViewHolder (to avoid having to use the workaround explained in the question).
My specific issue was due to a bad use of Kotlin's Data Classes. When using them, it is important to note that (as explained in the docs), the toString(), equals(), hashCode() and copy() will only take into account all those properties declared in the class' constructor, ignoring those declared in the class' body. A simple example:
data class MyClass1(val prop: Int, val name: String) {}
data class MyClass2(val prop: Int) {
var name: String = ""
}
fun main() {
val a = MyClass1(1, "a")
val b = MyClass1(1, "b")
println(a == b) //False :) -> a.name != b.name
val c = MyClass2(2)
c.name = "c"
val d = MyClass2(2)
d.name = "d"
println(c == d) //True!! :O -> But c.name != d.name
}
This is specially important when implementing the PagedListAdapter's DiffCallback, as if we are in a example's MyClass2 like scenario, no matter how many times we update the name field in our Room database, as the DiffCallback's areContentsTheSame() method is probably always going to return true, making the list never update on that change.
If the reason explained above is not the reason of your issue, or you just want to be able to observe LiveData instances properly from a ViewHolder, I developed a small library which provides a Lifecycle to any ViewHolder, making it able to observe LiveData instances the proper way (instead of having to use the workaround explained in the question).
https://github.com/Sarquella/LifecycleCells