OnComplete never called with toSortedList() and groupBy() - android

I'm currently using the Android-ReactiveLocation Library (Github). The LastKnownLocationObservable (Code) is working as intended. I'm using a flatMap to fetch nearby stations from a db and (because of realm) I'm creating a model from the data. So I have a list of items and I'm creating the new Observable in flatMap with Observable.from(data).
Then I want to sort the locations, filter them and group them.
.toSortedList()
.flatMap { Observable.from(it) }
.filter { it.distance <= (maxDistance.toDouble() * 1000) }
.groupBy { //Group the stations in categories
if (it.distance <= maxDistance && it.favorite) {
"nearbyFavorite"
} else if (it.favorite) {
"outOfReachFavorite"
} else {
"nearby"
}
}
However the onComplete is never called when I subscribe to the Observable. The Observable just stalls at toSortedList().
The Subscribe:
.subscribe(object: Subscriber<GroupedObservable<String, NearbyLocationItem>>() {
override fun onNext(p0: GroupedObservable<String, NearbyLocationItem>?) {
val locationItems = ArrayList<NearbyLocationItem>()
p0.subscribe { loc ->
locationItems.add(loc)
}
locations.put(p0.key, locationItems)
}
override fun onCompleted() {
Log.d(javaClass.simpleName, "Never called")
}
override fun onError(p0: Throwable?) {
}
}

Related

RxJava calling PublishSubject only once instead of multiple times

I'm trying to get data from an API paging service, after each call I have to call for new data by setting a new page number and once the API responds with an empty array I just have to stop the observable.
So I've tried making retrofit return an Observable and by subscribing to it
private fun getProducts(page: Int, lastId: Int): Observable<Response<List<ProdottoBarcode>>>? {
val observable = urlServer?.let {
RetrofitClient.getInstance()?.getService()?.getProducts(it, "A", page, lastId)
}
return observable
}
Then the subscription is made via PublicSubject in that way
private fun getAllProducts(): Observable<Response<List<ProdottoBarcode>>> {
// TODO: get response headers with MAX_PRODUCTS and return it to activity
var currentPage = 1
val subject: PublishSubject<Response<List<ProdottoBarcode>>> = PublishSubject.create()
return subject.doOnSubscribe {
getProducts(currentPage, 0)?.subscribe(subject)
}.doOnNext {
val products = it.body()
val lastId = it.headers().get("lastId")?.toInt()
if (products?.isEmpty() == true) {
subject.onComplete()
} else {
currentPage += 1
lastId?.let { id -> getProducts(currentPage, id)?.subscribe(subject) }
}
}
}
And I'm subscribing to the data in my handleMessage() in service:
override fun handleMessage(msg: Message) {
getAllProducts().subscribe {
val products = it.body()
if (products != null) {
for (product in products) {
scope.launch {
// TODO: return to activity counter of insert items
repository.insert(product)
}
}
}
}
stopSelf(msg.arg1)
}
The issue is that getAllProducts stops after the 2nd page is fetched even if there are other data...
So doOnNext() is made just twice.
It looks like you have a reentrancy and recursion problem.
You could just use range, concatMap the count to the service call, then make it stop when the result has an empty body:
int[] lastId = { 0 };
Observable.range(1, Integer.MAX_VALUE - 1)
.concatMap(currentPage -> getProducts(currentPage, lastId[0]))
.takeUntil(response -> response.body()?.products()?.isEmpty())
.doOnNext(response -> {
lastId[0] = response.headers().get("lastId")?.toInt();
});

RxJava Flowable.zip never returns a value

I have a code in my repository which has to call two endpoints. I have used Flowable.zip() but it doesn't seem to return a value. The Call doesn't fail even if there is no network available.
fun fetchRateRemote(): Flowable<ResultWrapper<List<RateModel>>> {
return Flowable.zip<Flowable<CurrenciesDTO>, Flowable<RateDTO>, ResultWrapper<List<RateModel>>>(
{
apiEndpoints.fetchCurrencies(key)
}, {
apiEndpoints.fetchRate(key)
}, { t1, t2 ->
val rateList = mutableListOf<RateModel>()
t2.subscribe { rate->
for((k,v) in rate.quotes ){
val currency = k.removeRange(0,3)
t1.subscribe {cur->
val currencyName = cur.currencies[currency]
if (currencyName != null) {
rateList.add(RateModel("$currencyName ($currency)", v.toString()))
}
}
}
}
ResultWrapper.Success(rateList)
}).subscribeOn(Schedulers.io())
}
I use a wrapper to mimic state and this is what I do in my viewmodel.
private fun fetchRates(){
disposable.add(repository.fetchRateRemote()
.startWith(ResultWrapper.Loading)
.onErrorReturn {
ResultWrapper.Error(it)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSubscriber<ResultWrapper<List<RateModel>>>() {
override fun onComplete() {}
override fun onNext(rate: ResultWrapper<List<RateModel>>) {
rates.postValue(rate)
}
override fun onError(error: Throwable) {
error.printStackTrace()
}
})
)
}
I then observe rate in my activity via LiveData. The wrapper or the observation isn't the issue. It works with other calls, I do not know why the zip call doesn't work. I'm fairly new to RxJava so If I didn't implement something correctly in my repository please help correct me.
Okay! I made a lot of mistakes with the code in the repository above but I managed to fix it. Here's the solution. The Type arguments for the zip method was wrong! I didn't call the BiFunction argument properly too.
fun fetchRateRemote(): Flowable<ResultWrapper<List<RateModel>>> {
return Flowable.zip<CurrenciesDTO, RateDTO, ResultWrapper<List<RateModel>>>(
apiEndpoints.fetchCurrencies(key), apiEndpoints.fetchRate(key), BiFunction { t1, t2 ->
val rateList = mutableListOf<RateModel>()
for((k,v) in t2.quotes ){
val currencyCode = k.removeRange(0,3)
val currencyName = t1.currencies[currencyCode]
if (currencyName != null) {
rateList.add(RateModel("$currencyName ($currencyCode)", v.toString()))
}
}
ResultWrapper.Success(rateList)
}).subscribeOn(Schedulers.io())
}

How to make two requests in one request with rxJava2

I am accessing the server in my Android app. I want to get a list of my friends and a list of friend requests in different queries. They have to come at the same time. Then I want to show this data on the screen.
I tried to get data from two queries at using flatMap.
interactor.getColleagues() and interactor.getTest() returns the data type Observable<List<Colleagues>>
private fun loadColleaguesEmployer() {
if (disposable?.isDisposed == true) disposable?.dispose()
//запрос на список друзей
interactor.getColleagues(view.getIdUser() ?: preferences.userId)
.subscribeOn(Schedulers.io())
.flatMap {
interactor.getTest().subscribeOn(Schedulers.io())
.doOnNext {
result-> view.showTest(mapper.map(result))
}
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = { result1 ->
//Обработка списка коллег работодателей
view.showColleagues(mapper.map(result1.filter { data -> data.typeFriend == "Работодатель" }))
},
onError = { it.printStackTrace() }
)
}
I want to get and process data from different queries at the same time.
Combining observable results of multiple async http requests with rxjava's Observable.zip.
public class Statistics {
public static void main(String[] args) {
List<Observable<ObservableHttpResponse>> observableRequests = Arrays.asList(
Http.getAsync("http://localhost:3001/stream"),
Http.getAsync("http://localhost:3002/stream"),
Http.getAsync("http://localhost:3003/stream"),
Http.getAsync("http://localhost:3004/stream"));
List<Observable<Stats>> observableStats = observableRequests.stream()
.map(observableRequest ->
observableRequest.flatMap(response ->
response.getContent()
.map(new EventStreamJsonMapper<>(Stats.class))))
.collect(toList());
Observable<List<Stats>> joinedObservables = Observable.zip(
observableStats.get(0),
observableStats.get(1),
observableStats.get(2),
observableStats.get(3),
Arrays::asList);
// This does not work, as FuncN accepts (Object...) https://github.com/Netflix/RxJava/blob/master/rxjava-core/src/main/java/rx/functions/FuncN.java#L19
// Observable<List<Stats>> joinedObservables = Observable.zip(observableStats, Arrays::asList);
joinedObservables
.take(10)
.subscribe(
(List<Stats> statslist) -> {
System.out.println(statslist);
double average = statslist.stream()
.mapToInt(stats -> stats.ongoingRequests)
.average()
.getAsDouble();
System.out.println("avg: " + average);
},
System.err::println,
Http::shutdown);
}
}
you can do it by simple operation zip like
private fun callRxJava() {
RetrofitBase.getClient(context).create(Services::class.java).getApiName()
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
getObservable()
.flatMap(object : io.reactivex.functions.Function<List<User>, Observable<User>> {
override fun apply(t: List<User>): Observable<User> {
return Observable.fromIterable(t); // returning user one by one from usersList.
} // flatMap - to return users one by one
})
.subscribe(object : Observer<User> {
override fun onSubscribe(d: Disposable) {
showProgressbar()
}
override fun onNext(t: User) {
userList.add(t)
hideProgressBar()
}
override fun onError(e: Throwable) {
Log.e("Error---", e.message)
hideProgressBar()
}
override fun onComplete() {
userAdapter.notifyDataSetChanged()
}
})
}
this function combines your response from 2 queries
private fun getObservable(): Observable<List<User>> {
return Observable.zip(
getCricketFansObservable(),
getFootlaballFansObservable(),
object : BiFunction<List<User>, List<User>, List<User>> {
override fun apply(t1: List<User>, t2: List<User>): List<User> {
val userList = ArrayList<User>()
userList.addAll(t1)
userList.addAll(t2)
return userList
}
})
}
here is example of first observable
fun getCricketFansObservable(): Observable<List<User>> {
return RetrofitBase.getClient(context).create(Services::class.java).getCricketers().subscribeOn(Schedulers.io())
}
If both observables return the same data type and you don't mind mixing of both sources data - consider using Observable.merge()
For example:
Observable.merge(interactor.getColleagues(), interactor.getTest())
.subscribeOn(Schedulers.io())
.subscribe(
(n) -> {/*do on next*/ },
(e) -> { /*do on error*/ });
Note, that .merge() operator doesn't care about emissions order.
Zip combine the emissions of multiple Observables together via a
specified function
You can use Zip (rx Java) http://reactivex.io/documentation/operators/zip.html, some sudo code will be like this -
val firstApiObserver = apIs.hitFirstApiFunction(//api parameters)
val secondApiObserver = apIs.hitSecondApiFunction(//api parameters)
val zip: Single<SubscriptionsZipper>//SubscriptionsZipper is the main model which contains first& second api response model ,
zip = Single.zip(firstApiObserver, secondApiObserver, BiFunction { firstApiResponseModel,secondApiResponseModel -> SubscriptionsZipper(firstApiResponseModelObjectInstance, secondApiResponseModelObjectInstance) })
zip.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(object : SingleObserver<SubscriptionsZipper> {
override fun onSubscribe(d: Disposable) {
compositeDisposable.add(d)
}
override fun onSuccess(subscriptionsZipper: SubscriptionsZipper) {
Utils.hideProgressDialog()
//here you will get both api response together
}
override fun onError(e: Throwable) {
Utils.hideProgressDialog()
}
})
Hope it helps you .

Firebase realtime snapshot listener using Coroutines

I want to be able to listen to realtime updates in Firebase DB's using Kotlin coroutines in my ViewModel.
The problem is that whenever a new message is created in the collection my application freezes and won't recover from this state. I need to kill it and restart app.
For the first time it passes and I can see the previous messages on the UI. This problem happens when SnapshotListener is called for 2nd time.
My observer() function
val channel = Channel<List<MessageEntity>>()
firestore.collection(path).addSnapshotListener { data, error ->
if (error != null) {
channel.close(error)
} else {
if (data != null) {
val messages = data.toObjects(MessageEntity::class.java)
//till this point it gets executed^^^^
channel.sendBlocking(messages)
} else {
channel.close(CancellationException("No data received"))
}
}
}
return channel
That's how I want to observe messages
launch(Dispatchers.IO) {
val newMessages =
messageRepository
.observer()
.receive()
}
}
After I replacing sendBlocking() with send() I am still not getting any new messages in the channel. SnapshotListener side is executed
//channel.sendBlocking(messages) was replaced by code bellow
scope.launch(Dispatchers.IO) {
channel.send(messages)
}
//scope is my viewModel
How to observe messages in firestore/realtime-dbs using Kotlin coroutines?
I have these extension functions, so I can simply get back results from the query as a Flow.
Flow is a Kotlin coroutine construct perfect for this purposes.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
#ExperimentalCoroutinesApi
fun CollectionReference.getQuerySnapshotFlow(): Flow<QuerySnapshot?> {
return callbackFlow {
val listenerRegistration =
addSnapshotListener { querySnapshot, firebaseFirestoreException ->
if (firebaseFirestoreException != null) {
cancel(
message = "error fetching collection data at path - $path",
cause = firebaseFirestoreException
)
return#addSnapshotListener
}
offer(querySnapshot)
}
awaitClose {
Timber.d("cancelling the listener on collection at path - $path")
listenerRegistration.remove()
}
}
}
#ExperimentalCoroutinesApi
fun <T> CollectionReference.getDataFlow(mapper: (QuerySnapshot?) -> T): Flow<T> {
return getQuerySnapshotFlow()
.map {
return#map mapper(it)
}
}
The following is an example of how to use the above functions.
#ExperimentalCoroutinesApi
fun getShoppingListItemsFlow(): Flow<List<ShoppingListItem>> {
return FirebaseFirestore.getInstance()
.collection("$COLLECTION_SHOPPING_LIST")
.getDataFlow { querySnapshot ->
querySnapshot?.documents?.map {
getShoppingListItemFromSnapshot(it)
} ?: listOf()
}
}
// Parses the document snapshot to the desired object
fun getShoppingListItemFromSnapshot(documentSnapshot: DocumentSnapshot) : ShoppingListItem {
return documentSnapshot.toObject(ShoppingListItem::class.java)!!
}
And in your ViewModel class, (or your Fragment) make sure you call this from the right scope, so the listener gets removed appropriately when the user moves away from the screen.
viewModelScope.launch {
getShoppingListItemsFlow().collect{
// Show on the view.
}
}
What I ended up with is I used Flow which is part of coroutines 1.2.0-alpha-2
return flowViaChannel { channel ->
firestore.collection(path).addSnapshotListener { data, error ->
if (error != null) {
channel.close(error)
} else {
if (data != null) {
val messages = data.toObjects(MessageEntity::class.java)
channel.sendBlocking(messages)
} else {
channel.close(CancellationException("No data received"))
}
}
}
channel.invokeOnClose {
it?.printStackTrace()
}
}
And that's how I observe it in my ViewModel
launch {
messageRepository.observe().collect {
//process
}
}
more on topic https://medium.com/#elizarov/cold-flows-hot-channels-d74769805f9
Extension function to remove callbacks
For Firebase's Firestore database there are two types of calls.
One time requests - addOnCompleteListener
Realtime updates - addSnapshotListener
One time requests
For one time requests there is an await extension function provided by the library org.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X. The function returns results from addOnCompleteListener.
For the latest version, see the Maven Repository, kotlinx-coroutines-play-services.
Resources
Using Firebase on Android with Kotlin Coroutines by Joe Birch
Using Kotlin Extension Functions and Coroutines with Firebase by Rosário Pereira Fernandes
Realtime updates
The extension function awaitRealtime has checks including verifying the state of the continuation in order to see whether it is in isActive state. This is important because the function is called when the user's main feed of content is updated either by a lifecycle event, refreshing the feed manually, or removing content from their feed. Without this check there will be a crash.
ExtenstionFuction.kt
data class QueryResponse(val packet: QuerySnapshot?, val error: FirebaseFirestoreException?)
suspend fun Query.awaitRealtime() = suspendCancellableCoroutine<QueryResponse> { continuation ->
addSnapshotListener({ value, error ->
if (error == null && continuation.isActive)
continuation.resume(QueryResponse(value, null))
else if (error != null && continuation.isActive)
continuation.resume(QueryResponse(null, error))
})
}
In order to handle errors the try/catch pattern is used.
Repository.kt
object ContentRepository {
fun getMainFeedList(isRealtime: Boolean, timeframe: Timestamp) = flow<Lce<PagedListResult>> {
emit(Loading())
val labeledSet = HashSet<String>()
val user = usersDocument.collection(getInstance().currentUser!!.uid)
syncLabeledContent(user, timeframe, labeledSet, SAVE_COLLECTION, this)
getLoggedInNonRealtimeContent(timeframe, labeledSet, this)
}
// Realtime updates with 'awaitRealtime' used
private suspend fun syncLabeledContent(user: CollectionReference, timeframe: Timestamp,
labeledSet: HashSet<String>, collection: String,
lce: FlowCollector<Lce<PagedListResult>>) {
val response = user.document(COLLECTIONS_DOCUMENT)
.collection(collection)
.orderBy(TIMESTAMP, DESCENDING)
.whereGreaterThanOrEqualTo(TIMESTAMP, timeframe)
.awaitRealtime()
if (response.error == null) {
val contentList = response.packet?.documentChanges?.map { doc ->
doc.document.toObject(Content::class.java).also { content ->
labeledSet.add(content.id)
}
}
database.contentDao().insertContentList(contentList)
} else lce.emit(Error(PagedListResult(null,
"Error retrieving user save_collection: ${response.error?.localizedMessage}")))
}
// One time updates with 'await' used
private suspend fun getLoggedInNonRealtimeContent(timeframe: Timestamp,
labeledSet: HashSet<String>,
lce: FlowCollector<Lce<PagedListResult>>) =
try {
database.contentDao().insertContentList(
contentEnCollection.orderBy(TIMESTAMP, DESCENDING)
.whereGreaterThanOrEqualTo(TIMESTAMP, timeframe).get().await()
.documentChanges
?.map { change -> change.document.toObject(Content::class.java) }
?.filter { content -> !labeledSet.contains(content.id) })
lce.emit(Lce.Content(PagedListResult(queryMainContentList(timeframe), "")))
} catch (error: FirebaseFirestoreException) {
lce.emit(Error(PagedListResult(
null,
CONTENT_LOGGED_IN_NON_REALTIME_ERROR + "${error.localizedMessage}")))
}
}
This is working for me:
suspend fun DocumentReference.observe(block: suspend (getNextSnapshot: suspend ()->DocumentSnapshot?)->Unit) {
val channel = Channel<Pair<DocumentSnapshot?, FirebaseFirestoreException?>>(Channel.UNLIMITED)
val listenerRegistration = this.addSnapshotListener { value, error ->
channel.sendBlocking(Pair(value, error))
}
try {
block {
val (value, error) = channel.receive()
if (error != null) {
throw error
}
value
}
}
finally {
channel.close()
listenerRegistration.remove()
}
}
Then you can use it like:
docRef.observe { getNextSnapshot ->
while (true) {
val value = getNextSnapshot() ?: continue
// do whatever you like with the database snapshot
}
}
If the observer block throws an error, or the block finishes, or your coroutine is cancelled, the listener is removed automatically.

multiple async-await in kotlin

obj in promoType = [list of string]
its more like 10 firebase queries are running here, looking in 10 particular set of nodes and going down further.
what i'm not sure, whether i require to put on async / await on each of the queries, but all i want is 10 of these queries to run and then result me in whether a couponKey is empty or not. All i want to do is to display whether a coupon entered was correct or not.
further, in changeUserType(couponKey, couponFoundAtKey), some database write operations occur.
fun checkPromo(promoCodeET: String) = async(UI) {
try {
val database = PersistentFirebaseUtil.getDatabase().reference
val job = async(CommonPool) {
for (obj in promoType) {
val query = database.child("promos").child(obj).orderByChild("promoCode").equalTo(promoCodeET)
query.addListenerForSingleValueEvent(object :
ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
couponKey = dataSnapshot.key.toString()
couponFoundAtKey = dataSnapshot.children.first().key.toString()
if (couponKey.isNotEmpty())
changeUserType(couponKey, couponFoundAtKey)
flag = true
}
}
override fun onCancelled(error: DatabaseError) {
// Failed to read value
}
})
if (flag) break
}
}
job.await()
}
catch (e: Exception) {
}
finally {
if (couponKey.isEmpty()){
Toast.makeText(this#Coupon, "Invalid coupon", Toast.LENGTH_LONG).show()
}
flag = true
}
}
There are several things I find wrong with your code:
You have an outer async(UI) which doesn't make sense
Your inner async(CommonPool) doesn't make sense either, because your database call is already async
You use the antipattern where you immediately await after async, making it not really "async" (but see above, the whole thing is async with or without this)
Your fetching function has a side-effect of changing the user type
To transfer the results to the caller, you again use side-effects instead of the return value
Your code should be much simpler. You should declare a suspend fun whose return value is the pair (couponKey, coupon):
suspend fun fetchPromo(promoType: String, promoCodeET: String): Pair<String, String>? =
suspendCancellableCoroutine { cont ->
val database = PersistentFirebaseUtil.getDatabase().reference
val query = database.child("promos").child(promoType)
.orderByChild("promoCode").equalTo(promoCodeET)
query.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
cont.resume(
dataSnapshot
.takeIf { it.exists() }
?.let { snapshot ->
snapshot.key.toString()
.takeIf { it.isNotEmpty() }
?.let { key ->
Pair(key, snapshot.children.first().key.toString())
}
}
)
}
override fun onCancelled(error: DatabaseError?) {
if (error != null) {
cont.resumeWithException(MyException(error))
} else {
cont.cancel()
}
}
})
}
To call this function, use a launch(UI) at the call site. Change the user type once you get a non-null value:
launch(UI) {
var found = false
for (type in promoType) {
val (couponKey, coupon) = fetchPromo(type, "promo-code-et") ?: continue
found = true
withContext(CommonPool) {
changeUserType(couponKey, coupon)
}
break
}
if (!found) {
Toast.makeText(this#Coupon, "Invalid coupon", Toast.LENGTH_LONG).show()
}
}
You say that changeUserType performs some database operations, so I wrapped them in a withContext(CommonPool).
Note also that I extracted the loop over promo types outside the function. This will result in queries being performed sequentially, but you can just write different calling code to achieve parallel lookup:
var numDone = 0
var found = false
promoType.forEach { type ->
launch(UI) {
fetchPromo(type, "promo-code-et")
.also { numDone++ }
?.also { (couponKey, coupon) ->
found = true
launch(CommonPool) {
changeUserType(couponKey, coupon)
}
}
?: if (numDone == promoType.size && !found) {
Toast.makeText(this#Coupon, "Invalid coupon", Toast.LENGTH_LONG).show()
}
}
}

Categories

Resources