Kotlin passive - active item rows in recycleview - android

I want to create a recyclerview list with active and passive rows, how can I do this in Kotlin? If the colum from the table in the Room Database is not the same as the itemtext, it will be inactive and cannot be clicked.
For example, while one item is clickable, the other cannot be clicked because this line will be inactive. The data from the other activity will not match the data saved in the database.Of course, it will not be able to do this comparison at first because the database is empty. Is such a situation possible with Kotlin?
I wrote the Recycleview list with an adapter. I haven't created the dao part yet.

Is Depend on which architecture you are using and how you are querying the data
Could you please specify more.

I have only single question why do need to capture invalidated data in recycler view? What we can do is just update our data and remove other data. But even if you want to do that then here is a trick.
Add a time field to your model.
data class Student(
val id :Int,
val name :String,
val lastUpdateTime:Long= System.currentTimeInMillies()
){
fun isOutdated():Boolean{
return THREE_MINUTES_IN_MILLIES<(System.currentTimeInMillies()-this.lastUpdateTime);
}
companion object{
private const val THREE_MINUTES_IN_MILLIES=3*60*1000;
}
}
Now don't change the business logic of your http-caching.
Now in your adapter's viewHolder class, add following logic.
inner class ViewHolder2(val binding: NoticeViewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(student: Student) {
if(student.isOutdated()){
//outdated list element
}else{
//fresh list element
}
}
}
Hopefully you are not updating your ui that fast. This should work. And if it does work then please accept my answer, I am new to answering SO questions. Love from India. ❤️

Related

Android Repository pattern - how to update a nested recycler view item from view model

I am using ViewModel and repository pattern to fetch the data in the list. The items are arranged as a list of rows and products. A Row class has products in it. The products can be scrolled horizontally. I am using a recycler view with linear layout manager (horizontal orientation) nested inside another recycler view (vertical orientation). Fetching the items via a ViewModel and rendering in the recycler views is pretty straightforward. The challenge is when I try to update the items (their counts) when an item is added to the cart. When the button (plus sign) is clicked, a callback is sent to the view model via a listener. The horizontal adapter sends the request back to the container (vertical) adapter, and the vertical adapter sends it back to the view model.
// The horizontal adapter
class SimpleProductAdapter(
private val shopId: String,
private val listener: (product: CartProduct) -> Unit
) :
ListAdapter<CartProduct, RecyclerView.ViewHolder>(...DiffCallbackGoesHere) {
// ... some more things here
fun bind(item: CartProduct?) {
view.add_to_cart_button.setOnClickListener {
listener(item)
}
}
The vertical adapter has similar structure
class RowAdapter(
private val shopId: String,
private val listener: (product: CartProduct) -> Unit
) :
PagedListAdapter<Row, RecyclerView.ViewHolder>(...RowDiffCallbackGoesHere) {
// ... some more things here
fun bind(item: Row?) {
SimpleProductAdapter(shopId) { product ->
listener(product)
}
}
And the main site inside the fragment where the viewmodel calls exist:
val rowAdapter = RowAdapter(args.shopId) { product->
if (actionType == ADD_TO_CART_ACTION) viewModel.buy(product)
.observe(viewLifecycleOwner, Observer {
view.swipe.isRefreshing = it is Resource.Loading
// I want to update the quantity here on success result
if(Resource is Success) {}
})
When the result is Success, I want to update the quantity; two things are challenging here
I am using PagedListAdapter from the paging library which gives me a (supposedly) immutable list of products.
Even if I update the PagedList and issue a notifyDataSetChanged, it would be too much to do that just to change a single count over a large set of items.
I am hoping to find a way where I can easily target the specific product to update or another alternative that I keep seeing on the web is to build a custom layout manager so that I can have a single adapter that draws everything on a single pass without having to nest the recycler views. That way updating the item would become easier (couldn't find a code example on this).
Any suggestions please.
In Paging3, there are plans to eventually add an API to support granular updates without invalidation: https://issuetracker.google.com/160232968
For now, you must invalidate in order to update the backing dataset. In general, DiffUtil will do a pretty good job of hiding this from the user though.

get LiveData Transformations in loop android

I am getting all categories from a different endpoint, different repository. That is giving me all categories. Now i want to run this categories in loop and write observer to get all the items of each category.
In other words -
I want to get FeedItems of all categories one by one because i have to fire separate endpoint to each category. I have list of all categories.
I am able to get single category's items by writing below code, but do not know how to get multiple.
In viewModal i am doing below code for to single category Observer
val categoryByIdItems: LiveData<Resource<List<FeedItem>>> = Transformations
.switchMap(categoryByIdParam) { input ->
input.ifExists { catId, shouldFetch ->
feedRepository.loadCategoryByIdFeed(catId.toString(), shouldFetch)
}
}
if i use above code it will give me feedItems of one category by calling load function
fun load(categories:List<Category>,shouldFetch:Boolean = false){
setCategoryByIdRequest(categories[0].id, shouldFetch)
}
I want to run setCategoryByIdRequest in loop based on categories, i can do that.
I am using this code to get first category items in fragment file
viewModel.categoryByIdItems.observe(this, Observer { resource ->
})
if i run the loop for viewModel.categoryByIdItems.observe than layout adds in fragment but because of same observer all are same.
But i am not sure how can i get categoryByIdItems in loop as observe in fragment.
as categoryByIdItems i am defining in view-modal first and using that in fragment but i want that dynamic based on number of categories.
Sorry for my writing as i do not know how to explain more to tell you what i want. I am new with LiveData and Transformations.
I am not using DB, using apis to get data. let me know if you want to find more code about repository, view modal or fragment, i'll provide you.
I would advise you to get all category in a list first from DB and then using kotlin filter to fetch by ID like this
DAO
#Query("SELECT * FROM categories_table")
fun getAll(): LiveData<List>
VIEWMODEL
private val _categories = DAO.getAll()
val categories : LiveData<Category>
get() = _categories
ACTIVITY/ FRAGMENT
viewModel.categories .observe(viewLifecycleOwner, Observer { it ->
if(it!=null){
//filter your data
})

How to avoid requestModelBuild for every widgets in a recyclerview

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

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.

Categories

Resources