Android: collecting a Kotlin Flow inside another not emitting - android

I have got the following method:
operator fun invoke(query: String): Flow<MutableList<JobDomainModel>> = flow {
val jobDomainModelList = mutableListOf<JobDomainModel>()
jobListingRepository.searchJobs(sanitizeSearchQuery(query))
.collect { jobEntityList: List<JobEntity> ->
for (jobEntity in jobEntityList) {
categoriesRepository.getCategoryById(jobEntity.categoryId)
.collect { categoryEntity ->
if (categoryEntity.categoryId == jobEntity.categoryId) {
jobDomainModelList.add(jobEntity.toDomainModel(categoryEntity))
}
}
}
emit(jobDomainModelList)
}
}
It searches in a repository calling the search method that returns a Flow<List<JobEntity>>. Then for every JobEntity in the flow, I need to fetch from the DB the category to which that job belongs. Once I have that category and the job, I can convert the job to a domain model object (JobDomainModel) and add it to a list, which will be returned in a flow as the return object of the method.
The problem I'm having is that nothing is ever emitted. I'm not sure if I'm missing something from working with flows in Kotlin, but I don't fetch the category by ID (categoriesRepository.getCategoryById(jobEntity.categoryId)) it then works fine and the list is emitted.
Thanks a lot in advance!

I think the problem is that you're collecting infinite length Flows, so collect never returns. You should use .take(1) to get a finite Flow before collecting it, or use first().
The Flows returned by your DAO are infinite length. The first value is the first query made, but the Flow will continue forever until cancelled. Each item in the Flow is a new query made when the contents of the database change.
Something like this:
operator fun invoke(query: String): Flow<MutableList<JobDomainModel>> =
jobListingRepository.searchJobs(sanitizeSearchQuery(query))
.map { jobEntityList: List<JobEntity> ->
jobEntityList.mapNotNull { jobEntity ->
categoriesRepository.getCategoryById(jobEntity.categoryId)
.first()
.takeIf { it.categoryId == jobEntity.categoryId }
}
}
Alternatively, in your DAO you could make a suspend function version of getCategoryById() that simply returns the list.

Get an idea from the code below if your Kotlin coroutine flow gets lost with a continuation approximate peak alloc exception
fun test(obj1: Object,obj2: Object) = flow {
emit(if (obj1 != null) repository.postObj(obj1).first() else IgnoreObjResponse)
}.map { Pair(it, repository.postObj(obj2).first()) }

Related

Kotlin rxjava switchIfEmpty

I'm trying to understand the logic of switchIfEmpty operator. I will be very thankful for every explanation.
I have a local database (Room) and remote server. My goal is to implement logic with switchIfEmpty to check if there is data in local DB to take it and if local DB is empty to call from remote. The process starts in activity where I subscribe to Observable:
private fun subscribeOnDataChanges() = with(viewModel) {
requestNextPage()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
filmsAdapter.addItems(it)
}, {
errorEvent.value = ERROR_MSG
it.printStackTrace()
}).addTo(autoDisposable)
}
Then the methods in the View Model:
fun requestNextPage(): Observable<List<Film>> {
return requestPageOfFilms()
}
private fun requestPageOfFilms(): Observable<List<Film>> =
interactor.requestPageOfFilmsFromDataSource()
And, finally the method with switchIfEmpty in the Interactor:
fun requestPageOfFilmsFromDataSource(): Observable<List<Film>> {
return repo.getPageOfFilmsInCategory(category).filter { it.isNotEmpty() }.switchIfEmpty(
getFromRemote(category)
)
}
private fun getFromRemote(category: String): Observable<List<Film>> {
return convertSingleApiToObservableDtoList(
retrofitService.getFilms(
category, API.KEY, "ru-RU", NEXT_PAGE
)
)
}
I cannot understand the next things:
Why, when local db (repo) is NOT empty, getFromRemote() is called?
If local db is empty, why network call in the method getFromRemote() is not performed? No matters, that I subscribed in the activity? Because if I add the subscription inside the switchIfEmpty(), the network call is performed.
To answer your questions :
Why, when local db (repo) is NOT empty, getFromRemote() is called?
Because this function convertSingleApiToObservableDtoList() is being evaluated at Assembly Time and not Subscription Time (https://github.com/ReactiveX/RxJava#assembly-time). The result is that the Observable object is eagerly evaluated.
If local db is empty, why network call in the method getFromRemote() is not performed? No matters, that I subscribed in the activity? Because if I add the subscription inside the switchIfEmpty(), the network call is performed.
Because the upstream has not emitted a value yet or completed. You are conflating an empty List with an empty Observable (https://reactivex.io/documentation/operators/defaultifempty.html)
A better solution to your problem would be using the flatMap or even switchMap operator if you expect a chatty upstream and only care about latest result, something like :
fun requestPageOfFilmsFromDataSource(): Observable<List<Film>> {
return repo.getPageOfFilmsInCategory(category)
.flatMap { films ->
if (films.isNotEmpty()) {
Observable.just(films)
} else getFromRemote(category)
}
}

How to use Firestore databse : addSnapshotListener using await() in Kotlin?

I have a DAO class where I have fetchHubList method which fetches a collection of documents from cloud Firestore asynchronously using await(). This implementation used the "get()" method which I got to know later on does not fetch real-time updates. On trying to implement the code similarly using onSnapshotListener gives an error (which was quite expected to be honest, because get() and this methods return quite different things). Does anyone have any idea how to implement this?
How the code is currently:
suspend fun fetchHubList(): ArrayList<HubModel>? = try {
val hubList = ArrayList<HubModel>()
hubsListCollection.get().await().map { document ->
if (document != null) {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "the document id is ${document.id}")
val temp = HubModel(document.get("hubName").toString(),
document.id.toString(),
document.get("isAdmin") as Boolean)
hubList.add(temp)
// hubList.add(document.toObject(HubModel::class.java))
} else {
Log.d(TAG, "No such document")
}
}
And what I want to implement here (and which is totally erroneous):
suspend fun fetchHubList(): ArrayList<HubModel>? = try {
val hubList = ArrayList<HubModel>()
hubsListCollection.addSnapshotListener().await().map { document ->
if (document != null) {
Log.d(TAG, "Data fetch successful!")
Log.d(TAG, "the document id is ${document.id}")
val temp = HubModel(document.get("hubName").toString(),
document.id.toString(),
document.get("isAdmin") as Boolean)
hubList.add(temp)
// hubList.add(document.toObject(HubModel::class.java))
} else {
Log.d(TAG, "No such document")
}
}
I use this function in my ViewModel class to create a LiveData wrapped ArrayList:
val hubList = MutableLiveData<ArrayList<HubModel>>()
private val hubListDao = HubListDao()
init {
viewModelScope.launch {
hubList.value = hubListDao.fetchHubList()
}
}
Thanks in advance!
You don't need addSnapshotListener, just use get:
hubsListCollection.get().await()
In order to observe changes in your collection you can extend LiveData:
class CafeLiveData(
private val documentReference: DocumentReference
) : LiveData<Cafe>(), EventListener<DocumentSnapshot> {
private var snapshotListener: ListenerRegistration? = null
override fun onActive() {
super.onActive()
snapshotListener = documentReference.addSnapshotListener(this)
}
override fun onInactive() {
super.onInactive()
snapshotListener?.remove()
}
override fun onEvent(result: DocumentSnapshot?, error: FirebaseFirestoreException?) {
val item = result?.let { document ->
document.toObject(Cafe::class.java)
}
value = item!!
}
}
And expose it from your view model:
fun getCafe(id: String): LiveData<Cafe> {
val query = Firebase.firestore.document("cafe/$id")
return CafeLiveData(query)
}
As #FrankvanPuffelen already mentioned in his comment, there is no way you can use ".await()" along with "addSnapshotListener()", as both are two totally different concepts. One is used to get data only once, while the second one is used to listen to real-time updates. This means that you can receive a continuous flow of data from the reference you are listening to.
Please notice that ".await()" is used in Kotlin with suspend functions. This means that when you call ".await()", you start a separate coroutine, which is a different thread that can work in parallel with other coroutines if needed. This is called async programming because ".await()" starts the coroutine execution and waits for its finish. In other words, you can use ".await()" on a deferred value to get its eventual result, if no Exception is thrown. Unfortunately, this mechanism doesn't work with real-time updates.
When it comes to Firestore, you can call ".await()" on a DocumentReference object, on a Query object, or on a CollectionReference object, which is actually a Query without filters. This means that you are waiting for the result/results to be available. So you can get a document or multiple documents from such calls. However, the following call:
hubsListCollection.addSnapshotListener().await()
Won't work, as "addSnapshotListener()" method returns a ListenerRegistration object.
I want to use a snapshot listener to listen to changes that might occur in my database to update my RecyclerView
In this case, you should consider using a library called Firebase-UI for Android. In this case, all the heavy work will be done behind the scenes. So there is no need for any coroutine or ".await()" calls, everything is synched in real-time.
If you don't want to use either Kotlin Coroutines, nor Firebase-UI Library, you can use LiveData. A concrete example can be seen in my following repo:
https://github.com/alexmamo/FirestoreRealtimePagination/blob/master/app/src/main/java/ro/alexmamo/firestorerealtimepagination/ProductListLiveData.java
Where you can subclass LiveData class and implement EventListener the interface.

Emit coroutine Flow from Room while backfilling via network request

I have my architecture like so:
Dao methods returning Flow<T>:
#Query("SELECT * FROM table WHERE id = :id")
fun itemById(id: Int): Flow<Item>
Repository layer returning items from DB but also backfilling from network:
(* Need help here -- this is not working as intended **)
fun items(): Flow<Item> = flow {
// Immediately emit values from DB
emitAll(itemDao.itemById(1))
// Backfill DB via network request without blocking coroutine
itemApi.makeRequest()
.also { insert(it) }
}
ViewModel layer taking the flow, applying any transformations, and converting it into a LiveData using .asLiveData():
fun observeItem(): LiveData<Item> = itemRepository.getItemFlow()
.map { // apply transformation to view model }
.asLiveData()
Fragment observing LiveData emissions and updating UI:
viewModel.item().observeNotNull(viewLifecycleOwner) {
renderUI(it)
}
The issue I'm having is at step 2. I can't seem to figure out a way to structure the logic so that I can emit the items from Flow immediately, but also perform the network fetch without waiting.
Since the fetch from network logic is in the same suspend function it'll wait for the network request to finish before emitting the results downstream. But I just want to fire that request independently since I'm not interested in waiting for a result (when it comes back, it'll update Room and I'll get the results naturally).
Any thoughts?
EDIT
Marko's solution works well for me, but I did attempt a similar approach like so:
suspend fun items(): Flow<List<Cryptocurrency>> = coroutineScope {
launch {
itemApi.makeRequest().also { insert(it) }
}
itemDao.itemById(1)
}
It sounds like you're describing a background task that you want to launch. For that you need access to your coroutine scope, so items() should be an extension function on CoroutineScope:
fun CoroutineScope.items(): Flow<Item> {
launch {
itemApi.makeRequest().also { insert(it) }
}
return flow {
emitAll(itemDao.itemById(1))
}
}
On the other hand, if you'd like to start a remote fetch whose result will also become a part of the response, you can do it as follows:
fun items(): Flow<Item> = flow {
coroutineScope {
val lateItem = async { itemApi.makeRequest().also { insert(it) } }
emitAll(itemDao.itemById(1))
emit(lateItem.await())
}
}

RxJava2 doOnNext returns later than intended

So I'm pretty sure that I'm kind of at a loss here.
The expected behavior is:
Get data from API -> save it in the local DB -> load data from the
local DB and display it
First of all in my Fragment I call a function that does this:
mLiveData = viewModel.fetchAllCategories(getString(R.string.lang_code))
mCategoryLiveData!!.observe(this, Observer<Array<Category>> { it ->
if (it != null) {
this#CategoryListFragment.allCategories = SparseArray(it.size)
for (category in it) {
allCategories[category.id] = category
}
displayedCategory = allCategories[1]
this#CategoryListFragment.mLoadingCircle.visibility = View.GONE
this#CategoryListFragment.displayCategoryChildren()
}
})
fetchAllCategories calls a function in the ViewModel which calls this function:
fun getAllCategoriesFromAPI(language: String): Flowable<Array<Category>> {
return service.getAllCategories(language)
.doOnNext {
Log.e("Repository", "Fetched ${it.size} Categories from the API ")
storeCategoriesInDb(it)
}
}
However the function displayCategoryChildren() fires before the onNext finishes which results in an error since the data the app is supposed to get from the db is not saved there yet.
If it is in any way relevant I can also post the fuction in the ViewModel

Implementing search that pushes results to list as soon as they become available using rxJava

I need to implement a search on a large data set that can take some time to complete on mobile devices. So I want to display each matching result as soon as it becomes available.
I need to fetch all available data from a data store that decides whether to get them from network or from the device. This call is an Observable. As soon as the data from that Observable becomes available I want to loop over it, apply a search predicate and notify any Observers for any match found.
So far my idea was to use a PublishSubject to subscribe to and call its onNext function every time the search finds a new match. However I can't seem to get the desired behavior to work.
I'm using MVVM + Android Databinding and want to display every matched entry in a RecyclerView so for every onNext event that is received by the observing viewModel I have to call notifyItemRangeInserted on the RecyclerView's adapter.
class MySearch(val dataStore: MyDataStore) {
private val searchSubject = PublishSubject.create<List<MyDto>>()
fun findEntries(query: String): Observable<List<MyDto>> {
return searchSubject.doOnSubscribe {
// dataStore.fetchAll returns an Observable<List<MyDto>>
dataStore.fetchAll.doOnNext {
myDtos -> if (query.isNotBlank()) {
search(query, myDtos)
} else {
searchSubject.onNext(myDtos)
}
}.subscribe(searchSubject)
}
}
private fun(query: String, data: List<MyDto>) {
data.forEach {
if (it.matches(query)) {
// in real life I cache a few results and don't send each single item
searchSubject.onNext(listOf(it))
}
}
}
fun MyDto.matches(query: String): Boolean // stub
}
-
class MyViewModel(val mySearch: MySearch, val viewNotifications: Observer<Pair<Int, Int>>): BaseObservable() {
var displayItems: List<MyItemViewModel> = listOf()
fun loadData(query: String): Subscription {
return mySearch.findEntries(query)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(this::onSearchResult)
.doOnCompleted(viewNotifications::onCompleted)
.doOnError(viewNotifications::onError)
.subscribe()
}
private fun onSearchResult(List<MyDto> data) {
val lastIndex = displayItems.lastIndex
displayItems = data.map { createItem(it) }
notifyChange()
viewNotifications.onNext(Pair(lastIndex, data.count()))
}
private fun createItem(dto: MyDto): MyItemViewModel // stub
}
The problem I have with the above code is that with an empty query MyViewModel::onSearchResult is called 3 times in a row and when the query is not empty MyViewModel::onSearchResult isn't called at all.
I suspect the problem lies somewhere in the way I have nested the Observables in findEntries or that I'm subscribing wrong / getting data from a wrong thread.
Does anyone have an idea about this?

Categories

Resources