How Can I Merge a Single<List<List<T>>> Into a List<T> with RxJava 2? - kotlin

What I want to do is hit an endpoint to get a list of users, that returns Single<List<User>>. Next, I want to grab the first three users and hit another endpoint to get all of their posts Single<List<Post>>. Finally I want to display a Toast that has the total number of posts for all of the first 3 users.
I've been able to achieve it with the flatten() function available in Kotlin. However, I'd like to know how to do this using only RxJava 2. Is it possible? Thanks.
...
getPostsForFirstThreeUsers()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ posts -> toast("There are: ${posts.flatten().size} posts") },
{ ex -> Timber.e(ex, "there was an error processing the request") }
)
fun getFirstThreeUsers(): Flowable<User> {
return getAllUsers()
.flattenAsFlowable { users -> users }
.doOnNext { Timber.i("a user: ${it.username}") }
.take(3)
}
fun getPostsForFirstThreeUsers(): Single<List<List<Post>>> {
return getFirstThreeUsers()
.flatMapSingle { api.getUsersPosts(it.id) }
.doOnNext { Timber.i("number of posts: ${it.size}") }
.toList()
}

Since you're only looking for the total number of posts, you could just flatmap the lists to a stream and then count. The flat-mapping will not guarantee order, but flattens the list of list into a single stream.
getPostsForFirstThreeUsers()
.subscribeOn(Schedulers.io())
.flatmapObservable(Observable::fromIterable)
.flatmap(Observable::fromIterable)
.count()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ count -> toast("There are: ${count} posts") },
{ ex -> Timber.e(ex, "there was an error processing the request") }
)

I haven't tested this but I think you can do this with flatmap and reduce (or collect). probably something's like:
getAllUsers()
.take(3)
.flatMap { api.getUsersPosts(it.id) }
.map { it.size }
.reduce(0) { acc, e -> acc + e }
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ posts -> toast("There are: ${posts.flatten().size} posts") },
{ ex -> Timber.e(ex, "there was an error processing the request") }
)

I ended up going with this solution, which allows me to have access to the entire List of Post objects. Thanks for the direction.
fun getPostsForFirstThreeUsers(): Single<MutableList<Post>> {
return getFirstThreeUsers()
.flatMapSingle { api.getUsersPosts(it.id) }
.doOnNext { Timber.i("number of posts: ${it.size}") }
.flatMapIterable { it }
.doOnNext { Timber.i("the post: $it") }
.toList()
}

Related

Kotlin RxJava Call API in loop one by one, terminate the loop if any API response has desire result

I want to call API for each item in list and when the response received has desire result will return and terminate the loop.
My current code:
Observable.from(riffInPlayerSubscription)
.flatMap { item ->
API.getAccessCheck(accessToken, item.itemId)
}.flatMap { itemAccessResponse ->
if (!itemAccessResponse.isExpired()) {
credentialStore.hasActiveSubscription = true
return#flatMap Observable.just(true)
}
}
But it loops for all items and doesn't terminate.
Use skipWhile and take:
Observable.from(riffInPlayerSubscription)
.flatMap { item ->
API.getAccessCheck(accessToken, item.itemId)
}
.skipWhile { it.isExpired() }
.take(1)
.map {
credentialStore.hasActiveSubscription = true
true
}

Equivalent of RxJava .toList() in Kotlin coroutines flow

I have a situation where I need to observe userIds then use those userIds to observe users. Either userIds or users could change at any time and I want to keep the emitted users up to date.
Here is an example of the sources of data I have:
data class User(val name: String)
fun observeBestUserIds(): Flow<List<String>> {
return flow {
emit(listOf("abc", "def"))
delay(500)
emit(listOf("123", "234"))
}
}
fun observeUserForId(userId: String): Flow<User> {
return flow {
emit(User("${userId}_name"))
delay(2000)
emit(User("${userId}_name_updated"))
}
}
In this scenario I want the emissions to be:
[User(abc_name), User(def_name)], then
[User(123_name), User(234_name)], then
[User(123_name_updated), User(234_name_updated)]
I think I can achieve this in RxJava like this:
observeBestUserIds.concatMapSingle { ids ->
Observable.fromIterable(ids)
.concatMap { id ->
observeUserForId(id)
}
.toList()
}
What function would I write to make a flow that emits that?
I believe you're looking for combine, which gives you an array that you can easily call toList() on:
observeBestUserIds().collectLatest { ids ->
combine(
ids.map { id -> observeUserForId(id) }
) {
it.toList()
}.collect {
println(it)
}
}
And here's the inner part with more explicit parameter names since you can't see the IDE's type hinting on Stack Overflow:
combine(
ids.map { id -> observeUserForId(id) }
) { arrayOfUsers: Array<User> ->
arrayOfUsers.toList()
}.collect { listOfUsers: List<User> ->
println(listOfUsers)
}
Output:
[User(name=abc_name), User(name=def_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]
Live demo (note that in the demo, all the output appears at once, but this is a limitation of the demo site - the lines appear with the timing you'd expect when the code is run locally)
This avoids the (abc_name_updated, def_name_updated) discussed in the original question. However, there's still an intermediate emission with 123_name_updated and 234_name because the 123_name_updated is emitted first and it sends the combined version immediately because they're the latest from each flow.
However, this can be avoided by debouncing the emissions (on my machine, a timeout as small as 1ms works, but I did 20ms to be conservative):
observeBestUserIds().collectLatest { ids ->
combine(
ids.map { id -> observeUserForId(id) }
) {
it.toList()
}.debounce(timeoutMillis = 20).collect {
println(it)
}
}
which gets you the exact output you wanted:
[User(name=abc_name), User(name=def_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]
Live demo
This is unfortunatly non trivial with the current state of kotlin Flow, there seem to be important operators missing. But please notice that you are not looking for rxJavas toList(). If you would try to to do it with toList and concatMap in rxjava you would have to wait till all observabes finish.
This is not what you want.
Unfortunately for you I think there is no way around a custom function.
It would have to aggregate all the results returned by observeUserForId for all the ids which you would pass to it. It would also not be a simple windowing function, since in reality it is conceivable that one observeUserForId already returned twice and another call still didn't finish. So checking whether you already have the same number of users as you passed ids into your aggregating functions isn't enought, you also have to group by user id.
I'll try to add code later today.
Edit: As promised here is my solution I took the liberty of augmenting the requirements slightly. So the flow will emit every time all userIds have values and an underlying user changes. I think this is more likely what you want since users probably don't change properties in lockstep.
Nevertheless if this is not what you want leave a comment.
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
data class User(val name: String)
fun observeBestUserIds(): Flow<List<String>> {
return flow {
emit(listOf("abc", "def"))
delay(500)
emit(listOf("123", "234"))
}
}
fun observeUserForId(userId: String): Flow<User> {
return flow {
emit(User("${userId}_name"))
delay(2000)
emit(User("${userId}_name_updated"))
}
}
inline fun <reified K, V> buildMap(keys: Set<K>, crossinline valueFunc: (K) -> Flow<V>): Flow<Map<K, V>> = flow {
val keysSize = keys.size
val valuesMap = HashMap<K, V>(keys.size)
flowOf(*keys.toTypedArray())
.flatMapMerge { key -> valueFunc(key).map {v -> Pair(key, v)} }
.collect { (key, value) ->
valuesMap[key] = value
if (valuesMap.keys.size == keysSize) {
emit(valuesMap.toMap())
}
}
}
fun observeUsersForIds(): Flow<List<User>> {
return observeBestUserIds().flatMapLatest { ids -> buildMap(ids.toSet(), ::observeUserForId as (String) -> Flow<User>) }
.map { m -> m.values.toList() }
}
fun main() = runBlocking {
observeUsersForIds()
.collect { user ->
println(user)
}
}
This will return
[User(name=def_name), User(name=abc_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]
You can run the code online here
You can use flatMapConcat
val users = observeBestUserIds()
.flatMapConcat { ids ->
flowOf(*ids.toTypedArray())
.map { id ->
observeUserForId(id)
}
}
.flattenConcat()
.toList()
or
observeBestUserIds()
.flatMapConcat { ids ->
flowOf(*ids.toTypedArray())
.map { id ->
observeUserForId(id)
}
}
.flattenConcat()
.collect { user ->
}

kotlin using rxjava2 map operator to modify a list of users

Kotlin 1.3.31
RxJava2
I have the following method that should get a list of users and loops through using the map operator and increase the gpa by 10. Then print the result out in the onNext. However, what I am getting printed is this:
kotlin.Unit
I was thinking that the list of users should be passed down the stream to the onNext in the subscribe method
private fun getUserListMapped() {
val disposable = getUserListFromCallable()
.map {
it.forEach { user ->
user.gpa *= 10
}
}
.subscribeOn(schedulerProvider.background())
.observeOn(schedulerProvider.ui())
.subscribe { println("users $it") }
}
This is what I am doing to get my users:
private fun getUserListFromCallable(): Observable<List<User>> {
return Observable.fromCallable { createListOfUsers() }
}
private fun createListOfUsers(): List<User> {
Thread.sleep(500L) // simulate getting from the network or local
return listOf(
User("john", "paris town", 5.6F),
User("simon", "hollands place", 2.56F),
User("lisa", "london bridge", 3.89F),
User("peter", "tokyo hills", 4.3F))
}
Many thanks for any suggestions
in your map you have to return value:
.map {
it.forEach { user ->
user.gpa *= 10
}
it
}
or you can just use doOnEach like this:
.doOnEach {
it.forEach { user ->
user.gpa *= 10
}
}
forEach method returns an Unit and that is why you see Unit printed. You should change the map operator to something like this:
.map { it.map { user -> user.apply { gpa *= 10 } } }

RxJava - Retrofit dont making request with Rx

I'm studying about RxJava with Retrofit, and I'm trying to combine two requests. But its not making a request to getToken api. It's a simple code just for study case
This is what I have now, what am I doing wrong?
apiManager.getToken(body)
.subscribeOn(Schedulers.io())
.map { people -> saveUser(people) }
.doOnNext { car -> Log.d("car",car.toString()) }
.flatMap { car -> Observable.from(car!!.items) }
.flatMap { carId -> val header = HashMap<String, String>()
header.put("Authorization", "Bearer " + user!!.authorization)
apiManager.getCarItens(header, carId.id!!) }
.doOnCompleted { showUser(user) }
.subscribeOn(AndroidSchedulers.mainThread())
You are just defining your Observable, but you are not subscribing to it, so the stream won't get data at all:
apiManager.getToken(body)
.subscribeOn(Schedulers.io())
.map { people -> saveUser(people) }
.doOnNext { car -> Log.d("car", car.toString()) }
.flatMap { car -> Observable.from(car!!.items) }
.flatMap { carId ->
val header = HashMap<String, String>()
header.put("Authorization", "Bearer " + user!!.authorization)
apiManager.getCarItens(header, carId.id!!)
}
.doOnCompleted { showUser(user) }
.subscribeOn(AndroidSchedulers.mainThread())
.subscribe() // This is what you were missing!
I'd recommend you cleanup some things, though:
Avoid using !!: it will cause an Exception if the objects are null, which will bubble up, given that you are not handling them (you could, as an onError parameter to subscribe
Instead of doOnCompleted, use onCompleted parameter to subscribe, which makes it be next to the error handling, and it's easier to read

RxJava2 Maybe return empty Observable if no element

Is there a better / more idiomatic way to use a Maybe type from JavaRx 2 than flatMap and try/catch? The following example takes a Maybe<User> and tries to book them a random ticket for a flight. If the user doesn't exist, return an empty Observable.
fun bookRandomTicketFor(userId: UUID): Observable<Ticket> {
val agencies = travelAgents() // Observable<TravelAgency>
val user = findById(userId) // Maybe<User>
val location = locate() // Observable<GeoLocation>
return Observable
.just(user.toObservable())
.flatMap { usr ->
try {
usr.zipWith(location, { aUser, location ->
agencies
.flatMap { agency ->
agency
.search(aUser, location) // Observable<Flight>.
.toList() // Convert to List<Flight>.
.toObservable() // And to Observable<List<Flight>>.
.flatMap { flights -> // So it can be shuffled,
Observable.just( // giving a random order.
shuffle(flights as MutableList<Flight>)[0]
)
}
}.firstElement() // Now take the first randomly shuffled Flight.
}).flatMap { flight ->
book(user.toObservable(), flight.toObservable())
}
} catch (ex: Exception) {
Observable.empty<Ticket>()
}
}
.doOnSubscribe { Logger.log("Random ticket: start for $userId") }
.doOnComplete { Logger.log("Random ticket: exit for $userId") }
}
It seems a bit of a fudge to have to convert the Maybe<User> to an Observable and start with an Observable<Observable<User>> that I can then flatMap and try/catch. Just wondering if there is a neater approach to doing this?