get LiveData Transformations in loop android - 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
})

Related

How to toggle between different Snapshot Listeners with Firestore?

I'm making a chat app and I'm using Firestore for real-time conversations. I have a simple snapshot listener and it's working fine, but I need to add filters according to different message types (e. g. images or audio). To reach this I'm using multiple Firestore queries to filter the content, the problem is that as I have multiple listeners, my RecyclerView gets updated with all of them.
I've tried adding the current listener into a variable, then detaching and adding the new one with the filters, but once I remove the first listener and add them again, they're not triggered anymore.
How can I toggle between different listeners, remove or add the one I need and only listen to it?
This is my ViewModel, getChatMessages() is the default query and getImageMessages is called when the image filter is applied:
//Current listener
private var listenerRegistration: ListenerRegistration? = null
//Get all messages
fun getChatMessages(id: Int) {
//Remove current listener
listenerRegistration?.remove()
val chatId = "chatroom_$id"
val query = db.collection(chatId).orderBy("timestamp", Query.Direction.DESCENDING)
listenerRegistration = query.addSnapshotListener { value, error ->
//Handle errors and update LiveData with messages
}
}
//Get only image messages
fun getImageMessages(id: Int) {
//Remove current listener
listenerRegistration?.remove()
val chatId = "chatroom_$id"
val query = db.collection(chatId).orderBy("timestamp", Query.Direction.DESCENDING)
.whereEqualTo("typeMessage", "image")
listenerRegistration = query.addSnapshotListener { value, error ->
//Handle errors and update LiveData with messages
}
}
I have a simple snapshot listener and it's working fine, but I need to add filters according to different message types (e. g. images or audio).
The following query works fine:
val query = db.collection(chatId).orderBy("timestamp", Query.Direction.DESCENDING)
Because it's a simple query that doesn't require something special to be done. However, the following query:
query = db.collection(chatId).orderBy("timestamp", Query.Direction.DESCENDING)
.whereEqualTo("typeMessage", "image")
Requires an index, since you're ordering the results on a different field than you perform the whereEqualTo() call. To solve this, please my answer in the following post:
Firestore whereEqualTo, orderBy and limit(1) not working

Kotlin passive - active item rows in recycleview

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. ❤️

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)
}
}
}

Android MVVM/ViewModel for RecyclerView with infinite scrolling (load more) - How to handle data on configuration change

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.

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