1

I am developing an app that shows a list of items it fetches from the internet. I have 2 buttons loadMore and refresh, loadMore - loads the next batch of items, refresh - load the items from the beginning.

I am using MVI (Model View Intent) pattern. Just to make it simple i created an example using a list of numbers each number will represent a batch of items:

    val loadSubject = BehaviorSubject.create<Unit>()
    val refreshSubject = PublishSubject.create<Unit>()

    val list = loadSubject.scanWith(
            { Observable.just(emptyList<Int>()) },
            { listObservable, _ ->
                listObservable
                        .map { it + ++count }
                        .replay().autoConnect()
            }
    )
            .flatMap { it }
            .filter { it.isNotEmpty() }


    val listSubscription = {
        list.subscribe {
            //do whatever with the list
        }
    }

    refreshSubject.scanWith(
            listSubscription,
            { disposable, _ ->
                disposable.dispose()
                listSubscription()
            }
    ).subscribe()

So now it would work perfectly but the subscription is in my Intent, I need a method with Rx that would do exact same thing but letting my View subscribe.

What I am trying to get is:

let say my list is [1,2,3]

on loadMore press ill get [1,2,3,4]

on refresh press ill get [5]

Dmitry Ryadnenko
  • 22,222
  • 4
  • 42
  • 56
CodePLeX
  • 133
  • 1
  • 2
  • 8

2 Answers2

0
val loadSubject = PublishSubject.create<Unit>()
val refreshSubject = PublishSubject.create<Unit>()

val loadMoreSequence = defer {
  loadSubject.scanWith(
    { Observable.just(listOf(0)) },
    { listObservable, _ ->
      listObservable
        .map { it + it.size }
        .replay().autoConnect()
    }
  )
    .flatMap { it }
}

val list = concat(Unit.asObservable(), refreshSubject)
  .switchMap { loadMoreSequence }

// testing

list.subscribe {
  println(it)
}

loadSubject.onNext(Unit)
loadSubject.onNext(Unit)
refreshSubject.onNext(Unit)
loadSubject.onNext(Unit)

result:

[0]
[0, 1]
[0, 1, 2]
[0]
[0, 1]
Dmitry Ryadnenko
  • 22,222
  • 4
  • 42
  • 56
0

I managed to get something working:

    val loadSubject = PublishSubject.create<Unit>()
    val refreshSubject = PublishSubject.create<Unit>()

    val loadList: (ItemsProvider) -> Observable<SingleEvent<List<Int>>> = { provider ->
        val markedGet = Observable.fromCallable(provider::fetchItem)
                .markStartAndEnd()

        loadSubject.concatWith(Unit)
                .flatMapWithDrop(markedGet)
                .publish().autoConnect()
    }

    val listEvents = refreshSubject.concatWith(Unit)
            .switchMap { loadList(itemsProvider) }

    val list = refreshSubject.concatWith(Unit)
            .switchMap {
                listEvents.filter { it is SingleEvent.Result }
                        .map { (it as SingleEvent.Result).data }
                        .scan { list: List<Int>, newList: List<Int> ->
                            list + newList
                        }
            }

    val isListEmpty = list.map { it.isEmpty() }

    val isDownloading = listEvents.map { it.isRunning() }

    val isRefreshing =
            Observable.merge(
                    refreshSubject.map { true },
                    isDownloading.filter { !it }.skip(1)
            )

The extensions are:

fun <T> Observable<T>.concatWith(item: T): Observable<T> =
        Observable.concat(Observable.just(item), this)

sealed class SingleEvent<T> {
    class Start<T> : SingleEvent<T>()
    data class Result<T>(val data: T) : SingleEvent<T>()
}

fun <T> Observable<T>.markStartAndEnd(): Observable<SingleEvent<T>> {
    return this.map { SingleEvent.Result(it) as SingleEvent<T> }
            .startWith(SingleEvent.Start())
}

/**
* Flatmaps upstream items into [source] items.
* Ignores upstream items if there is any [source] instance is currently running.
*
* upstream ----u-----u---u-------u---------------|-->
*              ↓                 ↓               ↓
* source       ---s-------|->    ---s-------|->  ↓
*                 ↓                 ↓            ↓
* result   -------s-----------------s------------|-->
*/
fun <T, R> Observable<T>.flatMapWithDrop(source: Observable<R>): Observable<R> =
        this.toFlowable(BackpressureStrategy.DROP)
                .flatMap({ source.toFlowable(BackpressureStrategy.MISSING) }, 1)
                .toObservable()

Based on Dmitry Ryadnenko blog post.

But i cant get flatMapWithDrop to work, because im doing network calls i need to drop any incoming refresh request until the current refresh request will finishes.

update: is there any other way to cache the list (without using refreshSubject.concatWith(Unit).switchMap {} again) ?

Dmitry Ryadnenko
  • 22,222
  • 4
  • 42
  • 56
CodePLeX
  • 133
  • 1
  • 2
  • 8