ViewHolder with information from multiple database tables - android

I am trying to have my RecyclerView / ViewHolder / ListAdapter hold information from two different tables, but cannot figure out how to do so.
My two tables
BudgetCategory
Title (Primary Key)
Budget
Transaction
ID (PK)
Category Title (Foreign Key)
Amount Spent
Based on the Android Kotlin Fundamentals course, I currently have a RecyclerView / ViewHolder / ListAdapter that displays a list of BudgetCategory items. This works great. However, I believe the view would be more useful if I could also display the current amount spent in the same ViewHolder (like the picture below). My problem is, I don't know how to properly perform a query of the Transaction table to get the amount spent in a BudgetCategory and update the ViewHolder without performing that query on the main UI thread. Especially because, the query of the Transaction table is entirely based on what BudgetCategory object the ViewHolder currently has.
That is the summary of the problem. Here is everything I have tried so far.
Inside my ViewHolder I tried to make a call like:
binding.spent = transactionDao.getSpentInCategory(category.title, startDate, endDate).value
Of course, the problem with this is the query returns LiveData<Double> which is an asynchronous call, so when I call .value on it, there is nothing there yet, so the "$Spent" in the image above is always blank. I also couldn't figure out how to observe that so the view would know to update itself when there was actually something in the LiveData.
I then tried to create a new data class BudgetCategoryOverview which had a BudgetCategory and a Double for the amount spent. I then changed my ListAdapter and ViewHolder to use BudgetCategoryOverview instead of BudgetCategory. To populate BudgetCategoryOverview I tried two different things:
In my Fragment in observe method of my LiveData<BudgetCategory> I tried to populate the list of BudgetCategoryOverview. This failed with an exception that I cannot make DB queries on the main UI thread.
In the ViewModel I tried to use suspending functions to populate a list (code shown below), but this failed because in the populateBudgetCategoryOverviewList() categories.value was always null.
lateinit var categories = LiveData<List<BudgetCategory>>
var budgetCategories = MutableLiveData<MutableList<BudgetCategoryOverview>>()
init{
budgetCategories.value = mutableListOf()
getBudgetCategoriesAndSpent()
}
private fun getBudgetCategoriesAndSpent(){
uiScope.launch{
updateBudgetCategories()
populateBudgetCategoryOverviewList()
}
}
private suspend fun updateBudgetCategories(){
withContext(Dispatchers.IO){
categories = budgetCategoryDao.getAllBudgetCategories()
}
}
private suspend fun populateBudgetCategoryOverviewList(){
withContext(Dispatchers.IO){
budgetCategories.value?.clear()
for( category in categories.value!! ){
val spent = transactionDao.getSpentInCategory(category.title, startDate, endDate)
budgetCategories.value!!.add(BudgetCategoryOverview(category, spent))
}
}
}
The last thing I tried was to create a #DatabaseView class, but the problem with this is the amount spent is based on a startDate and an endDate so I cannot create a database view based on a query statement that isn't a compile time constant.
I feel like I am searching for the wrong things online which is why I am here. What am I doing wrong? I am sure this isn't that uncommon of a use case. What is the standard approach to solve this?
Let me know if you need to see anymore code, but since I tried so many different things, I wasn't sure what versions of what code to post.
Thanks!

Related

Paging 3: How to load list at item position or at item with specific id

I have a list of messages.
Each message has a unique GUID.
My setup is working for normal usage: user clicks on conversation, list opens with all the messages belonging to that conversation, ordered by most recent first.
ConversationFragment
#Override
public void onViewCreated(
#NonNull View view,
#Nullable Bundle savedInstanceState
) {
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
viewModel
.getMessageList(lifecycleOwner, conversationId) // conversationId is a global variable
.observe(lifecycleOwner, messagePagingData -> adapter.submitData(
lifecycleOwner.getLifecycle(),
messagePagingData
));
super.onViewCreated(view, savedInstanceState);
}
ConversationViewModel
final PagingConfig pagingConfig = new PagingConfig(10, 10, false, 20);
private final ConversationRepository conversationRepository;
public ConversationViewModel(#NonNull Application application) {
super(application);
conversationRepository = new ConversationRepository(application);
}
public LiveData<PagingData<ItemMessage>> getMessageList(
#NonNull LifecycleOwner lifecycleOwner,
#NonNull String conversationId
) {
return PagingLiveData.cachedIn(
PagingLiveData.getLiveData(new Pager<>(pagingConfig, () -> conversationRepository.getMessageList(conversationId))),
lifecycleOwner.getLifecycle()
);
}
ConversationRepository
private final MessageDao messageDao;
public ConversationRepository(#NonNull Context context) {
AppDatabase database = AppDatabase.getDatabase(context);
messageDao = database.messageDao();
}
public PagingSource<Integer, ItemMessage> getMessageList(#NonNull String conversationId) {
return messageDao.getMessageList(conversationId);
}
MessageDao
#Query(
"SELECT * FROM Message " +
"WHERE Message.conversationId = :conversationId " +
"ORDER BY Message.time DESC"
)
public abstract PagingSource<Integer, ItemMessage> getMessageList(String conversationId);
Now my goal is to be able to open the conversation already scrolled at a specific message.
I also do not want to load the entire conversation and then scroll to the message, some conversations can be very long and I do not want to put the user on an auto scroll that can take ages to reach the specific message.
Ideally the way I envision this being done correct is to pass the message id to be in view, load a chunk of X messages surrounding before and after that message id and then after it is already presented to the user in the RecyclerView it will load more if the user goes up or down.
This is not meant to use network requests, the entire conversation is available in the database already so it will only use the information that is already in the database.
I've tried understanding the examples that use ItemKeyedDataSource or PageKeyedDataSource, but I cannot go anywhere because every single time those examples are in Kotlin only and require Retrofit to work, which I do not use. As it is these examples are completely useless for anyone like me that is in Java and not using Retrofit.
How can this be achieved?
Please provide an answer in Java, not just Kotlin only (kotlin is OK as long as it's in java as well) and please do not suggest new libraries.
As far as I could find the official documentation does not provide any sort of clue on how to solve this one for a Paging + Room integration. In fact, it doesn't provide any solution whatsoever to scroll to an item in a PagingDataAdapter, period.
The only thing that worked for me so far was to run two queries every single time I wish to accomplish this: one to find the item position in the result query list and the other to actually load said list with the initialKey set in the Pager constructor with the value of the item position we queried previously.
And if you're feeling a bit confused, this does not end here, because even the explanation for what is initialKey and how to use it is just not documented. No, seriously: What does the initialKey parameter do in the Pager constructor
So there's two guessing games here: one to find a proper way to lookup the item index from a result list and another to set it up properly in the final query.
I hope the Paging 3 documentation gets improved soon to cover these very basic issues.
In the end this is an example of how I managed to get this problem kind of working for me, even though I have no idea if this is the proper way to do it because, again, their documentation is absolutely lacking in this department.
Create two identical queries for the list results you desire
One of those queries only returns a full list of the results based on a key you'll use to uniquely identify an item. In my case it is messageId.
Load the query in 2 and individually iterate the results list using a for... loop until you find the item you want to know its position in the list. That position is given by the iterator you use in your loop block.
Pass the item position from 3 as initialKey parameter into your Pager builder of the final query
The first chunk of data you'll receive now will contain the item you want
If you want you can now scroll to that item in your RecyclerView, but you'll have to query it from the current list of items loaded in the adapter. See about using the .snapshot() in the PagingAdapter
That's it, now I can finally load an item at a certain position using Paging 3 + Room, with absolutely no idea of whether this is the proper way to do it thanks to the completely absent documentation for this.

Retrieve single record with Room to populate an Edit dialog (activity or fragment)

I have a database table which stores some records. I have been able to correctly populate a RecyclerView in a Fragment, following tutorials like this one and similar ones found via search engine.
What I want to do next is to tie an "Edit record {id}" fragment that is tied to the RecyclerView. In other words, if I click on an item in the Recycler view, another fragment(or activity) should open, load the data for record[id] from the database and then allow me to save and update the record if needed.
The point where I am stuck is retrieving the single record from the database, because I systematically end up with either (1) calling the query inside the main thread, which Room prevents me from doing, or (2) getting some random null pointer.
I have seen solutions even here on stackoverflow, but I can't make sense on how to integrate them in my case.
What I can't make sense of is how to make the async call (whether with threads/coroutines), store the result in a variable, and use it to populate the fields in the Edit fragment.
Internet search have been very disappointing, for all I find are (duplicate) tutorials that are either incomplete, irrelevant or obsolete.
Good pointers are welcome. I would prefer not to use third party libraries to do this, unless someone can explain to me the advantages in doing so.
Sorry for the long post: I haven't added code because there would be too many pieces to show and you would probably know anyway. I will answer any questions however to help out.
Also, I am new to Kotlin/Android, and I am trying to tame this beast :-)
Its hard to say anything specific without any code, but the correct way to do it would be
Retrieve all records from Room
Load them in your recycler view, so recycler adapter will have a list of all your records
setup on click listener in your recycler adapter to open the next activity or fragment
pass the primary key (as in room) of clicked item to the next activity or fragment
In your next activity retrieve a record from room using the primary key
bind the retrieved record to UI
If your recycler view and adapter are correctly setup then you should have following in your adapter
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
// dataList contains all your records as retrieved from room
// and loaded in your recycler view
setListeners(dataList[position], holder)
holder.bind(dataList[position])
}
private fun setListeners(selectedRecord: YourRecordTypeInRoom, viewHolder: YourViewHolder){
viewHolder.itemView.setOnClickListener {
var intent = Intent(viewHolder.itemView.context, NextActivity::class.java)
// pass primary key to next activity
intent.putExtra("primaryKey", selectedRecord.primaryKey)
viewHolder.itemView.context.startActivity(intent)
}
}
Now to retrieve your single record you should have something as follows in your dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): YourRecordType
Edit:
You can also modify the return type of above function to be a LiveData object, which will allow you to observe it in your activity in an async manner. with live data your code would look some thing as follows.
In Dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): LiveData<YourRecordType>
In your view model
fun getRecordByPrimaryKey(primaryKey: PrimaryKeyType) = yourDao.findByPrimaryKey(primaryKey)
and in your activity or fragment
viewModel.getRecordByPrimaryKey(primaryKey).observe(this, Observer{
// Bind your record on UI
})
1) Return fun someFunction():LiveData<Model> in Room class, (you should be able to call it from Main thread). After getting value once, you can stop observing, since you want only single value
2) You can use Kotlin Coroutines, this way you return suspend fun someFunction():Model. You can only call this function from another Coroutine, so it will be something like:
class ViewModel{
fun normalFunction(){
viewModelScope.launch{
val result = room.someFunction()
// tell View that you have result (View observes result using LiveData)
}
}
}

Room with LiveData: Many to Many mapping in adapter

Lets take the following example:
A many to many mapping exists for PRODUCTS and ORDERS. So a product can be on multiple orders and an order can have multiple products. In Room I have an entity which has both the product id and order id as foreign keys so I can save the relations. It's now very easy to get all the orders for a specific product and also all the products for a specific order.
Now here comes the trouble. As far as I know there is no way to get the order object with all of it's products in 1 query/entity. This can be read in further detail in this post. In most places I can bypass this by just running two queries. The first to get the order I'm interested in, and the second to get the products based on the Id of the order.
Now I want to display the combination of an order with its products in an adapter. For that I need to combine all my orders with their products. I'm clueless on how to solve this with LiveData.
The best solution in my opinion would be to create one query that fetches the OrderWithProducts directly from the database. This post suggests it should be possible, but I've not managed to get this to work. Also the most crucial part in that example is missing: the OrderItem class.
If that solution is not possible there must be some way to get the LiveData OrderWithProducts list with 2 queries and somehow combine them.
EDIT
After the suggestions of #Demigod now I have the following in my ViewModel:
// MediatorLiveData can observe other LiveData objects and react on their emissions.
var liveGroupWithLights = MutableLiveData<List<GroupWithLights>>()
fun createOrdersWithProducts() {
appExecutors.diskIO().execute {
val ordersWithProducts = mutableListOf<OrderWithProducts>()
val orders = orderRepository.getGroupsSync()
for (order in orders) {
val products = productRepository.getProductsSync(order.id)
val orderWithProducts = OrderWithProducts(order, products)
ordersWithProducts.add(orderWithProducts)
}
liveGroupWithLights.postValue(ordersWithProducts)
}
}
The function inside my fragment to submit data to the adapter:
private fun initRecyclerView() {
orderListViewModel.getOrdersWithProducts().observe(this, Observer { result ->
adapter.submitList(result)
})
}
So now I'm able to have a OrderWithProduct object as the item for my adapter. This is great, I can use products for each order in my adapter. Now I'm having trouble to update these items whenever the values in the database changes. Any ideas for this part?
Edit2: the invalidationtracker
db.invalidationTracker.addObserver(object : InvalidationTracker.Observer("orders", "products", "order_product_join") {
override fun onInvalidated(tables: MutableSet<String>) {
createOrdersWithProducts()
}
})
The problem I have now is that the validation tracker gets notified a lot for a single change.
As far as I know, it's not possible currently with a single query.
To solve this, you will need to run several queries here. At first - obtain a list of orders with a single query, and after that obtain a list of products per each order. To achieve this, I can think of several options:
Make your own OrdersWithProductsProvider, which will return this combined entities (Order with List<Porduct>), and it will subscribe for the changes to database to emit new objects using LiveData on every orders or products table change.
You can use a MediatorLiveData to fill the list of Orders with their Products, but I don't think this is a best approach since you will need to run query in a background thread, maybe use of Rx is more convenient here.
Personally, I would use a first option, since probably I want to obtain up-to-date list of orders with their products, which means that the update should trigger on change of three tables (products, orders, products_to_orders), which can be done via Room.InvalidationTracker. Inside that provider I would use Rx (which can work with LiveData via LiveDataReactiveStreams).
Addition on how to achieve that:
How to achieve that isn't really matters, the only thing - run this whole query in the background thread post it to LiveData. You can use Executor, Rx, or a simple Thread. So it will look something like:
private val database : Database // Get the DB
private val executor = Executors.newSingleThreadExecutor()
private val liveData = MutableLiveData<List<OrderWithProducts>>()
fun observeOrdersWithProducts():LiveData<List<OrderWithProducts>> {
return liveData
}
private fun updateOrdersWithProducts() {
executor.post {
val ordersWithProducts = mutableListOf<OrderWithProducts>()
val orders = db.orders()
for (order : orders) {
val products = database.productsForOrder(order)
val orderWithProducts = OrderWithProducts(order, products)
ordersWithProducts.add(orderWithProducts)
}
liveData.post(ordersWithProducts)
}
}
Take it as not complete working code, rather an example of implementation.
Call updateOrdersWithProducts on initialization/first call and every time InvalidationTracker will notify about the db change.

Paging Library with custom DataSource not updating row on Room update

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

ListAdapter not updating item in RecyclerView

I'm using the new support library ListAdapter. Here's my code for the adapter
class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent.inflate(R.layout.item_artist))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(artist: Artist) {
itemView.artistDetails.text = artist.artistAlbums
.plus(" Albums")
.plus(" \u2022 ")
.plus(artist.artistTracks)
.plus(" Tracks")
itemView.artistName.text = artist.artistCover
itemView.artistCoverImage.loadURL(artist.artistCover)
}
}
}
I'm updating the adapter with
musicViewModel.getAllArtists().observe(this, Observer {
it?.let {
artistAdapter.submitList(it)
}
})
My diff class
class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem?.artistId == newItem?.artistId
}
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
}
What's happening is when submitList is called the first time the adapter renders all the items, but when submitList is called again with updated object properties it does not re-render the view which has changed.
It re-renders the view as I scroll the list, which in turn calls bindView()
Also, I've noticed that calling adapter.notifyDatasSetChanged() after submit list renders the view with updated values, but I don't want to call notifyDataSetChanged() because the list adapter has diff utils built-in
Can anyone help me here?
Edit: I understand why this happens that wasn't my point. My point is that it at least needs to give a warning or call the notifyDataSetChanged() function. Because apparently I am calling the submitList(...) function for a reason. I am pretty sure people are trying to figure out what went wrong for hours until they figure out the submitList() ignores silently the call.
This is because of Googles weird logic. So if you pass the same list to the adapter it does not even call the DiffUtil.
public void submitList(final List<T> newList) {
if (newList == mList) {
// nothing to do
return;
}
....
}
I really don't understand the whole point of this ListAdapter if it can't handle changes on the same list. If you want to change the items on the list you pass to the ListAdapter and see the changes then either you need to create a deep copy of the list or you need to use regular RecyclerView with your own DiffUtill class.
The library assumes you are using Room or any other ORM which offers a new async list every time it gets updated, so just calling submitList on it will work, and for sloppy developers, it prevents doing the calculations twice if the same list is called.
The accepted answer is correct, it offers the explanation but not the solution.
What you can do in case you're not using any such libraries is:
submitList(null);
submitList(myList);
Another solution would be to override submitList (which doesn't cause that quick blink) as such:
#Override
public void submitList(final List<Author> list) {
super.submitList(list != null ? new ArrayList<>(list) : null);
}
Or with Kotlin code:
override fun submitList(list: List<CatItem>?) {
super.submitList(list?.let { ArrayList(it) })
}
Questionable logic but works perfectly.
My preferred method is the second one because it doesn't cause each row to get an onBind call.
with Kotlin just you need to convert your list to new MutableList like this or another type of list according to your usage
.observe(this, Observer {
adapter.submitList(it?.toMutableList())
})
I had a similar problem but the incorrect rendering was caused by a combination of setHasFixedSize(true) and android:layout_height="wrap_content". For the first time, the adapter was supplied with an empty list so the height never got updated and was 0. Anyway, this resolved my issue. Someone else might have the same problem and will think it is problem with the adapter.
If you encounter some issues when using
recycler_view.setHasFixedSize(true)
you should definitly check this comment:
https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531
It solved the issue on my side.
(Here is a screenshot of the comment as requested)
According to the official docs :
Whenever you call submitList it submits a new list to be diffed and displayed. This is why whenever you call submitList on the previous (already submitted list), it does not calculate the Diff and does not notify the adapter for change in the dataset.
Wasted so much time to figure out the problem in same case.
But in my situation the problem was that i forgot to specify a layoutManager for my recyclerView: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())
I hope no one will repeat my mistake...
Today I also stumbled upon this "problem".
With the help of insa_c's answer and RJFares's solution I made myself a Kotlin extension function:
/**
* Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
*
* Originally, [ListAdapter] will not update the view if the provided list is the same as
* currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
* could never work - the [ListAdapter] must have the previous list if items to compare new
* ones to using provided diff callback.
* However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
* the view to be updated. This extension function handles this case by making a copy of the
* list if the provided list is the same instance as currently loaded one.
*
* For more info see 'RJFares' and 'insa_c' answers on
* https://stackoverflow.com/questions/49726385/listadapter-not-updating-item-in-reyclerview
*/
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
// ListAdapter<>.submitList() contains (stripped):
// if (newList == mList) {
// // nothing to do
// return;
// }
this.submitList(if (list == this.currentList) list.toList() else list)
}
which can then be used anywhere, e.g.:
viewModel.foundDevices.observe(this, Observer {
binding.recyclerViewDevices.adapter.updateList(it)
})
and it only (and always) copies the list if it is the same as currently loaded one.
In my case I forgot to set the LayoutManager for the RecyclerView. The effect of that is the same as described above.
I got some strange behavior. I'm using MutableList in LiveDate.
In kotlin, the following codes don't work:
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it)
})
But, when I change it to it.toList(), it works
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it.toList())
})
Although, "it" was the same list.
For me, this issue appeared if I was using RecyclerView inside of ScrollView with nestedScrollingEnabled="false" and RV height set to wrap_content.
The adapter updated properly and the bind function was called, but the items were not shown - the RecyclerView was stuck at its' original size.
Changing ScrollView to NestedScrollView fixed the issue.
I had a similar problem. The issue was in the Diff functions, which didn't adequately compare the items. Anyone with this issue, make sure your Diff functions (and by extension your data object classes) contain proper comparison definitions - i.e. comparing all fields which might be updated in the new item. For example in the original post
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
This function (potentially) does not do what it says on the label: it does not compare the contents of the two items - unless you have overridden the equals() function in the Artist class. In my case, I had not, and the definition of areContentsTheSame only checked one of the necessary fields, due to my oversight when implementing it. This is structural equality vs. referential equality, you can find more about it here
The reason your ListAdapter .submitlist is not called is because the object
you updated still holds the same adress in memory.
When you update an object with lets say .setText it changes the value in the original object.
So that when you check if object.id == object2.id it will return as the same
because the both have a reference to the same location in memory.
The solution is to create a new object with the updated data and insert that in your list. Then submitList will be called and it will work correctly
It solve my problem. I think the best way is not to override submitList but add a new function to add new list.
fun updateList(list: MutableList<ScaleDispBlock>?) {
list?.let {
val newList = ArrayList<ScaleDispBlock>(list)
submitList(newList)
}
}
I also ran into similar issue, my usecase was i had a clickHandler and item will be selected/not selected (toggle on click).
I tried most of the approach from the above answers, only thing that worked is
adapter.submitList(null)
adapter.submitList(modifiedList)
but problem with this is everytime i click on any clickHandler the whole list is being redrawn again which is very ineffecient.
What i did ?
I made a live data that will store last clicked item and observing that live data, we can tell adapter that live data has been updated like below
viewModel.lastClicked.observe(viewLifeCycleOwner, {
adapter.notifyItemChanged(it)
}
Had a VERY similar issue, to this one, and decided to open a new thread and even create a GitHub project to mess around with. Most solutions didn't quite work for me, not even the toMutableList() way. In my case, the problem was solved by using immutable classes and submitting immutable Lists to the Adapter.
For anyone who's scenario is same as mine, I leave my solution, which I don't know why it's working, here.
The solution which worked for me was from #Mina Samir, which is submitting the list as a mutable list.
My Issue scenario :
-Loading a friend list inside a fragment.
ActivityMain attaches the FragmentFriendList(Observes to the livedata of friend db items) and on the same time, requests a http request to the server to get all of my friend list.
Update or insert the items from the http server.
Every change ignites the onChanged callback of the livedata. But, when it's my first time launching the application, which means that there was nothing on my table, the submitList succeeds without any error of any kind, but nothing appears on the screen.
However, when it's my second time launching the application, data are being loaded to the screen.
The solution is, as metioned above, submitting the list as a mutableList.
As has already been mentioned, you cannot submit a List with the same reference because the ListAdapter will see the lists are in the same location and will therefore not be able to use the DiffUtil.
The simplest solution would be to make a shallow copy of the list.
submitList(ArrayList(list))
Be wary converting the List to a MutableList, as that can create conditions for Exceptions and hard to find bugs.
this will work ....
what happen Is when you get the current list you are pointing to the same list at same location
I needed to modify my DiffUtils
override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
To actually return whether the contents are new, not just compare the id of the model.
Using #RJFares first answer updates the list successfully, but doesn't maintain the scroll state. The entire RecyclerView starts from 0th position. As a workaround, this is what I did:
fun updateDataList(newList:List<String>){ //new list from DB or Network
val tempList = dataList.toMutableList() // dataList is the old list
tempList.addAll(newList)
listAdapter.submitList(tempList) // Recyclerview Adapter Instance
dataList = tempList
}
This way, I'm able to maintain the scroll state of RecyclerView along with modified data.
Optimal Soltion:
for Kotlin
var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
list.add(Item("Content"))
adapter.submitList(list) {
Log.e("ListAdaptor","List Updated Successfully")
}
We should not maintain another base list as adapter.currentList will return a list in which diff is already calculated.
We have to provide a new instance every time a list updated because of DiffUtil
As per android documentation
DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one.
One list is already maintained by AsyncListDiffer which runs the diffutil on the background thread and another one has to be passed using adaptor.submitList()
The way that worked for me is to override the submitList() and create a copy of the incoming list and each item inside it too:
override fun submitList(list: List<Item>?) {
val listCopy =
mutableListOf<Item>().apply {
list?.map {
add(Item(it.id, it.name, it.imageUrl))
}
}
super.submitList(listCopy)
}
I encounter a very similar issue.
After the data list changed, I submit it again, the recycler view doesn't show as I wanted. It shows duplicated items.
I haven't found the root cause, but I find a workaround, that is to set the adapter to recycler view again. I guess this makes recycler viewer forget the memory before and render again correctly.
userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)
Once you have modify the array list, you have to let adapter know that which position that should be change
this code below is working in my case wish it may help
private fun addItem() {
val index = myArrayList.size
val position = myArrayList.size+1
myArrayList.add(
index, MyArrayClass("1", "Item Name")
)
myAdapter.notifyItemInserted(position) // in case of insert
// in case of remove item
// val index = myArrayList.size-1
// myAdapter.notifyItemRemoved(index)
}
just call adapter.notifyDataSetChanged() after differ.submitList
In my case i was using same object(from adadptar) to update Room database.
Create new object to update database and it'll fix the issue.
Example: I was doing this ->
val playlist = adapter.getItem(position)
playlist.name = "new name"
updatePlaylistObjectInRoomDatabase(playlist)
above code will change object in adapter before room database. So no change will be detected by DiffUtil callback.
Now doing this ->
val playlist = adapter.getItem(position)
val newPlaylist = Playlist()
newPlaylist.id = playlist.id
newPlaylist.name = "new name"
updatePlaylistObjectInRoomDatabase(newPlaylist)
Above code will not change anything in adapter list and will only change data in room database. so submitList will have different values DiffUtil callback can detect.
Enjoy the little things :)
This is something naturally expecte to be available on the official API, but as it isn't, this can be a way to deal with it:
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.clearItems() {
submitList(null)
submitList(emptyList())
}
The adapter can not understand that you have some updates, I don't know why!?
I am adding some entities to the list ad I m expected to collect them at the consumption point. But, nothing happens.
As a solution that worked for me you can use the script below:
artistAdapter.submitList(it.toMutableList())
Because the problem lays inside the ListAdapter, I would like to solve it inside the ListAdapter.
Thanks to Kotlin extension, we can write it like:
class MyItemAdapter() :
ListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) {
// ...
override fun submitList(list: List<Item>?) {
super.submitList(list?.toList())
}
}
It does look like a tricky hack. So I'd like to make a comment too:
super.submitList(list?.toList()) // to make submitList work, new value MUST be a new list. https://stackoverflow.com/a/50031492/9735961
And yes, thank you, RecyclerView developers.

Categories

Resources