From 0ddb61a454ce362bfc19c56587da92459b409d36 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Wed, 12 Oct 2016 09:07:13 -0700 Subject: [PATCH 01/73] chore: update readme with another pagination example --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e1016eba..b5d1a64d 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,10 @@ For kicks, I've also included a `PaginationAutoFragment` example, this "auto-pag Here are some other fancy implementations (while i enjoyed reading them, i didn't land up using them for my real world app cause personally i don't think it's necessary): * [Matthias example of an Rx based pager](https://gist.github.com/mttkay/24881a0ce986f6ec4b4d) +* [Eugene's very comprehensive Pagination sample](https://github.com/matzuk/PaginationSample) * [Recursive Paging example](http://stackoverflow.com/questions/28047272/handle-paging-with-rxjava) + ## Work in Progress: Examples that I would like to have here, but haven't found the time yet to flush out. From 035836185f60faa62427742a96f692bad8d35e99 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Wed, 12 Oct 2016 22:55:08 -0700 Subject: [PATCH 02/73] fix: use concatEager vs concat courtesy Jake's suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🙏 [Jedi Jake](https://twitter.com/JakeWharton/status/786362280535592961). --- README.md | 4 +++- .../android/rxjava/fragments/PseudoCacheConcatFragment.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e1016eba..df8c3f4c 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,12 @@ The value of this technique becomes more apparent when you have more number of i ### Retrieve data first from a cache, then a network call - using [`.concat`](http://reactivex.io/documentation/operators/concat.html) -Using concat, you can retrieve information from an observable first (presumably this one is fast like retrieveing from a disk cache) and show preliminary data to a user. Subsequently, when the longer running 2nd observable is complete (say a network call), you can update the results on the interface using the latest information. +Using concat, you can retrieve information from an observable first (presumably this one is fast like retrieving from a disk cache) and show preliminary data to a user. Subsequently, when the longer running 2nd observable is complete (say a network call), you can update the results on the interface using the latest information. For the purposes of illustration i use an in-memory `List` (not an actual disk cache), then shoot out a real network call to the github api so it gives you a feel of how this can really be applied in production apps. +Note the use of `concatEager` here over the traditional `concat` operator. Both show the results from the `Observables` in a sequential manner (so disk first and then network). The `concat` operator however would not even begin the subscription on subsequent Observables unless the first one is complete whereas `concatEager` kicks off all Observables at the time of Subscription in parallel, but still preserves order. + **Update:** After a [conversation I had with @artem_zin](https://twitter.com/kaushikgopal/status/591271805211451392), we arrived at an alternative solution to the same problem. One that used the [`.merge`](http://reactivex.io/documentation/operators/merge.html) operator instead. diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheConcatFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheConcatFragment.java index b895e0c8..98972ed7 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheConcatFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheConcatFragment.java @@ -57,7 +57,7 @@ public void onDemoPseudoCacheClicked() { _resultList.setAdapter(_adapter); _initializeCache(); - Observable.concat(_getCachedData(), _getFreshData()) + Observable.concatEager(_getCachedData(), _getFreshData()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber() { From 02e34617a23e4a23a6aa64cbacd9295599bebcaf Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Thu, 13 Oct 2016 06:53:18 -0700 Subject: [PATCH 03/73] --wip-- --- .../android/rxjava/fragments/MainFragment.java | 1 + .../fragments/PseudoCacheMergeFragment.java | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java index 9d542493..9730607a 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java @@ -74,6 +74,7 @@ void formValidation() { @OnClick(R.id.btn_demo_pseudo_cache) void pseudoCacheDemo() { + //clickedOn(new PseudoCacheConcatFragment()); clickedOn(new PseudoCacheMergeFragment()); } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java index fc21f5b8..79c762b8 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java @@ -21,6 +21,7 @@ import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; @@ -59,7 +60,10 @@ public void onDemoPseudoCacheClicked() { _resultList.setAdapter(_adapter); _initializeCache(); - Observable.merge(_getCachedData(), _getFreshData()) + + _getFreshData()// + .publish(fd -> Observable.merge(fd, _getCachedData().takeUntil(fd))) + //Observable.merge(_getCachedData(), _getFreshData()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber>() { @@ -77,13 +81,13 @@ public void onError(Throwable e) { public void onNext(Pair contributorAgePair) { Contributor contributor = contributorAgePair.first; - if (_resultAgeMap.containsKey(contributor) && - _resultAgeMap.get(contributor) > contributorAgePair.second) { - return; - } + //if (_resultAgeMap.containsKey(contributor) && + // _resultAgeMap.get(contributor) > contributorAgePair.second) { + // return; + //} _contributionMap.put(contributor.login, contributor.contributions); - _resultAgeMap.put(contributor, contributorAgePair.second); + //_resultAgeMap.put(contributor, contributorAgePair.second); _adapter.clear(); _adapter.addAll(getListStringFromMap()); @@ -117,7 +121,7 @@ private Observable> _getCachedData() { list.add(dataWithAgePair); } - return Observable.from(list); + return Observable.from(list).delay(5, TimeUnit.SECONDS); } private Observable> _getFreshData() { From 614c8b1348984b166c3b837f1640d03f69e9bfed Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Tue, 18 Oct 2016 22:42:05 -0700 Subject: [PATCH 04/73] fix: revert pseudo cache merge fragment to old code --- .../fragments/PseudoCacheMergeFragment.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java index 79c762b8..6fbaca18 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java @@ -8,20 +8,16 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; - +import butterknife.Bind; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.retrofit.Contributor; import com.morihacky.android.rxjava.retrofit.GithubApi; import com.morihacky.android.rxjava.retrofit.GithubService; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; @@ -60,10 +56,7 @@ public void onDemoPseudoCacheClicked() { _resultList.setAdapter(_adapter); _initializeCache(); - - _getFreshData()// - .publish(fd -> Observable.merge(fd, _getCachedData().takeUntil(fd))) - //Observable.merge(_getCachedData(), _getFreshData()) + Observable.merge(_getCachedData(), _getFreshData()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber>() { @@ -81,13 +74,13 @@ public void onError(Throwable e) { public void onNext(Pair contributorAgePair) { Contributor contributor = contributorAgePair.first; - //if (_resultAgeMap.containsKey(contributor) && - // _resultAgeMap.get(contributor) > contributorAgePair.second) { - // return; - //} + if (_resultAgeMap.containsKey(contributor) && + _resultAgeMap.get(contributor) > contributorAgePair.second) { + return; + } _contributionMap.put(contributor.login, contributor.contributions); - //_resultAgeMap.put(contributor, contributorAgePair.second); + _resultAgeMap.put(contributor, contributorAgePair.second); _adapter.clear(); _adapter.addAll(getListStringFromMap()); @@ -121,7 +114,7 @@ private Observable> _getCachedData() { list.add(dataWithAgePair); } - return Observable.from(list).delay(5, TimeUnit.SECONDS); + return Observable.from(list); } private Observable> _getFreshData() { From 605dc13e1e60ec45343c342f20d57a9e69edade5 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Tue, 18 Oct 2016 22:43:15 -0700 Subject: [PATCH 05/73] feat: update disk + network cache example inspiration from JW's twitter comment - https://twitter.com/JakeWharton/status/786363146990649345 --- README.md | 27 +- .../rxjava/fragments/MainFragment.java | 3 +- .../rxjava/fragments/PseudoCacheFragment.java | 301 ++++++++++++++++++ .../main/res/layout/fragment_pseudo_cache.xml | 112 +++++++ app/src/main/res/layout/item_log_white.xml | 16 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 9 + 7 files changed, 458 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheFragment.java create mode 100644 app/src/main/res/layout/fragment_pseudo_cache.xml create mode 100644 app/src/main/res/layout/item_log_white.xml diff --git a/README.md b/README.md index 63682d06..a77dcf41 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ I also gave a talk at a local meetup about warming up to RxJava here. Here's a l ### Concurrency using schedulers -A common requirement is to offload lengthy heavy I/O intensive operationsacc to a background thread (non-UI thread) and feed the results back to the UI/main thread, on completion. This is a demo of how long-running operations can be offloaded to a background thread. After the operation is done, we resume back on the main thread. All using RxJava! Think of this as a replacement to AsyncTasks. +A common requirement is to offload lengthy heavy I/O intensive operations to a background thread (non-UI thread) and feed the results back to the UI/main thread, on completion. This is a demo of how long-running operations can be offloaded to a background thread. After the operation is done, we resume back on the main thread. All using RxJava! Think of this as a replacement to AsyncTasks. The long operation is simulated by a blocking Thread.sleep call (since this is done in a background thread, our UI is never interrupted). @@ -101,21 +101,30 @@ Note that the `Func3` function that checks for validity, kicks in only after ALL The value of this technique becomes more apparent when you have more number of input fields in a form. Handling it otherwise with a bunch of booleans makes the code cluttered and kind of difficult to follow. But using `.combineLatest` all that logic is concentrated in a nice compact block of code (I still use booleans but that was to make the example more readable). -### Retrieve data first from a cache, then a network call - using [`.concat`](http://reactivex.io/documentation/operators/concat.html) +### Retrieve data first from a cache, then a network call -Using concat, you can retrieve information from an observable first (presumably this one is fast like retrieving from a disk cache) and show preliminary data to a user. Subsequently, when the longer running 2nd observable is complete (say a network call), you can update the results on the interface using the latest information. +We have two source (Observables): a disk (fast) cache and a network (fresh) call. The disk cache is much faster than the network Observable. But in order to demonstrate the working, I've also used a "slower" fake disk cache in order to demonstrate the working of the operators. -For the purposes of illustration i use an in-memory `List` (not an actual disk cache), then shoot out a real network call to the github api so it gives you a feel of how this can really be applied in production apps. +This is demonstrated using 4 techniques: -Note the use of `concatEager` here over the traditional `concat` operator. Both show the results from the `Observables` in a sequential manner (so disk first and then network). The `concat` operator however would not even begin the subscription on subsequent Observables unless the first one is complete whereas `concatEager` kicks off all Observables at the time of Subscription in parallel, but still preserves order. +1. [`.concat`](http://reactivex.io/documentation/operators/concat.html) +2. [`.concatEager`](http://reactivex.io/RxJava/javadoc/rx/Observable.html#concatEager(java.lang.Iterable)) +3. [`.merge`](http://reactivex.io/documentation/operators/merge.html) +4. [`.publish`](http://reactivex.io/RxJava/javadoc/rx/Observable.html#publish(rx.functions.Func1)) selector + merge + takeUntil -**Update:** +The 4th technique [courtesy Jedi JW](https://twitter.com/JakeWharton/status/786363146990649345) is probably what you want to use. But in order to understand why it's interested to go through the progression of techniques. -After a [conversation I had with @artem_zin](https://twitter.com/kaushikgopal/status/591271805211451392), we arrived at an alternative solution to the same problem. One that used the [`.merge`](http://reactivex.io/documentation/operators/merge.html) operator instead. +`concat` is great. It retrieves information from the first Observable (disk cache in our case) and then the subsequent network Observable. Since the disk cache is presumably faster, all appears well and the disk cache is loaded up fast, and once the network call finishes we swap out the "fresh" results. -The `concat` (and the equivalent [`startWith`](http://reactivex.io/documentation/operators/startwith.html)) opeartor is strictly sequential, meaning all of the items emitted by the first Observable are emitted strictly before any of the items from the second Observable are emitted. So assuming the first observable (for some strange reason) takes really long to run through all its items, even if the first few items from the second observable have come down the wire it will forcibly be queued. +The problem with `concat` is that the subsequent observable doesn't even start until the first Observable completes. That can be a problem. We want all observables to start simultaneously but produce the results in a way we expect. Thankfully RxJava introduced `concatEager` which does exactly that. It starts both observables but buffers the result from the latter one until the former Observable finishes. This is a completely viable option. -The `merge` operator on the other hand interleaves items as they are emitted. The problem here though is if for some strange reason an item is emitted by the cache or slower observable *after* the newer/fresher observable, it will overwrite the newer content. To account for this you have to monitor the "resultAge" somehow. This is demonstrated in the updated solution `PseudoCacheMergeFragment`. +Sometimes though, you just want to start showing the results immediately. Assuming the first observable (for some strange reason) takes really long to run through all its items, even if the first few items from the second observable have come down the wire it will forcibly be queued. You don't necessarily want to "wait" on any Observable. In these situations, we could use the `merge` operator. It interleaves items as they are emitted. This works great and starts to spit out the results as soon as they're shown. + +Similar to the `concat` operator, if your first Observable is always faster than the second Observable you won't run into any problems. However the problem with `merge` is: if for some strange reason an item is emitted by the cache or slower observable *after* the newer/fresher observable, it will overwrite the newer content. Click the "MERGE (SLOWER DISK)" button in the example to see this problem in action. @JakeWharton and @swankjesse contributions go to 0! In the real world this could be bad, as it would mean the fresh data would get overridden by stale disk data. + +To solve this problem you can use merge in combination with the super nifty `publish` operator which takes in a "selector". I wrote about this usage in a [blog post](http://blog.kaush.co/2015/01/21/rxjava-tip-for-the-day-share-publish-refcount-and-all-that-jazz/) but I have [Jedi JW](https://twitter.com/JakeWharton/status/786363146990649345) to thank for reminding of this technique. We `publish` the network observable and provide it a selector which starts emitting from the disk cache, up until the point that the network observable starts emitting. Once the network observable starts emitting, it ignores all results from the disk observable. This is perfect and handles any problems we might have. + +Previously, I was using the `merge` operator but overcoming the problem of results being overwritten by monitoring the "resultAge". See the old `PseudoCacheMergeFragment` example if you're curious to see this. ### Simple Timing demos using timer/interval/delay diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java index 9730607a..02ed2046 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java @@ -74,8 +74,7 @@ void formValidation() { @OnClick(R.id.btn_demo_pseudo_cache) void pseudoCacheDemo() { - //clickedOn(new PseudoCacheConcatFragment()); - clickedOn(new PseudoCacheMergeFragment()); + clickedOn(new PseudoCacheFragment()); } @OnClick(R.id.btn_demo_timing) diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheFragment.java new file mode 100644 index 00000000..9049d3f3 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheFragment.java @@ -0,0 +1,301 @@ +package com.morihacky.android.rxjava.fragments; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import butterknife.Bind; +import butterknife.ButterKnife; +import butterknife.OnClick; +import com.morihacky.android.rxjava.R; +import com.morihacky.android.rxjava.retrofit.Contributor; +import com.morihacky.android.rxjava.retrofit.GithubApi; +import com.morihacky.android.rxjava.retrofit.GithubService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import rx.Observable; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; +import timber.log.Timber; + +public class PseudoCacheFragment + extends BaseFragment { + + @Bind(R.id.info_pseudoCache_demo) TextView infoText; + @Bind(R.id.info_pseudoCache_listSubscription) ListView listSubscriptionInfo; + @Bind(R.id.info_pseudoCache_listDtl) ListView listDetail; + + private ArrayAdapter adapterDetail, adapterSubscriptionInfo; + private HashMap contributionMap = null; + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_pseudo_cache, container, false); + ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ButterKnife.unbind(this); + } + + @OnClick(R.id.btn_pseudoCache_concat) + public void onConcatBtnClicked() { + infoText.setText(R.string.msg_pseudoCache_demoInfo_concat); + wireupDemo(); + + Observable.concat(getSlowCachedDiskData(), getFreshNetworkData()) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Contributor contributor) { + contributionMap.put(contributor.login, contributor.contributions); + adapterDetail.clear(); + adapterDetail.addAll(mapAsList(contributionMap)); + } + }); + } + + @OnClick(R.id.btn_pseudoCache_concatEager) + public void onConcatEagerBtnClicked() { + infoText.setText(R.string.msg_pseudoCache_demoInfo_concatEager); + wireupDemo(); + + Observable.concatEager(getSlowCachedDiskData(), getFreshNetworkData()) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Contributor contributor) { + contributionMap.put(contributor.login, contributor.contributions); + adapterDetail.clear(); + adapterDetail.addAll(mapAsList(contributionMap)); + } + }); + + } + + @OnClick(R.id.btn_pseudoCache_merge) + public void onMergeBtnClicked() { + infoText.setText(R.string.msg_pseudoCache_demoInfo_merge); + wireupDemo(); + + Observable.merge(getCachedDiskData(), getFreshNetworkData()) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Contributor contributor) { + contributionMap.put(contributor.login, contributor.contributions); + adapterDetail.clear(); + adapterDetail.addAll(mapAsList(contributionMap)); + } + }); + } + + @OnClick(R.id.btn_pseudoCache_mergeSlowDisk) + public void onMergeSlowBtnClicked() { + infoText.setText(R.string.msg_pseudoCache_demoInfo_mergeSlowDisk); + wireupDemo(); + + Observable.merge(getSlowCachedDiskData(), getFreshNetworkData()) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Contributor contributor) { + contributionMap.put(contributor.login, contributor.contributions); + adapterDetail.clear(); + adapterDetail.addAll(mapAsList(contributionMap)); + } + }); + } + + @OnClick(R.id.btn_pseudoCache_mergeOptimized) + public void onMergeOptimizedBtnClicked() { + infoText.setText(R.string.msg_pseudoCache_demoInfo_mergeOptimized); + wireupDemo(); + + getFreshNetworkData()// + .publish(network ->// + Observable.merge(network,// + getCachedDiskData().takeUntil(network))) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Contributor contributor) { + contributionMap.put(contributor.login, contributor.contributions); + adapterDetail.clear(); + adapterDetail.addAll(mapAsList(contributionMap)); + } + }); + } + + @OnClick(R.id.btn_pseudoCache_mergeOptimizedSlowDisk) + public void onMergeOptimizedWithSlowDiskBtnClicked() { + infoText.setText(R.string.msg_pseudoCache_demoInfo_mergeOptimizedSlowDisk); + wireupDemo(); + + getFreshNetworkData()// + .publish(network ->// + Observable.merge(network,// + getSlowCachedDiskData().takeUntil(network))) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Subscriber() { + @Override + public void onCompleted() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Contributor contributor) { + contributionMap.put(contributor.login, contributor.contributions); + adapterDetail.clear(); + adapterDetail.addAll(mapAsList(contributionMap)); + } + }); + } + + // ----------------------------------------------------------------------------------- + // WIRING for example + + private void wireupDemo() { + contributionMap = new HashMap<>(); + + adapterDetail = new ArrayAdapter<>(getActivity(), R.layout.item_log_white, R.id.item_log, new ArrayList<>()); + listDetail.setAdapter(adapterDetail); + + adapterSubscriptionInfo = new ArrayAdapter<>(getActivity(), + R.layout.item_log_white, + R.id.item_log, + new ArrayList<>()); + listSubscriptionInfo.setAdapter(adapterSubscriptionInfo); + } + + private Observable getSlowCachedDiskData() { + return Observable.timer(1, TimeUnit.SECONDS).flatMap(dummy -> getCachedDiskData()); + } + + private Observable getCachedDiskData() { + List list = new ArrayList<>(); + Map map = dummyDiskData(); + + for (String username : map.keySet()) { + Contributor c = new Contributor(); + c.login = username; + c.contributions = map.get(username); + list.add(c); + } + + return Observable.from(list)// + .doOnSubscribe(() -> new Handler(Looper.getMainLooper())// + .post(() -> adapterSubscriptionInfo.add("(disk) cache subscribed")))// + .doOnCompleted(() -> new Handler(Looper.getMainLooper())// + .post(() -> adapterSubscriptionInfo.add("(disk) cache completed"))); + } + + private Observable getFreshNetworkData() { + String githubToken = getResources().getString(R.string.github_oauth_token); + GithubApi githubService = GithubService.createGithubService(githubToken); + + return githubService.contributors("square", "retrofit") + .flatMap(Observable::from) + .doOnSubscribe(() -> new Handler(Looper.getMainLooper())// + .post(() -> adapterSubscriptionInfo.add("(network) subscribed")))// + .doOnCompleted(() -> new Handler(Looper.getMainLooper())// + .post(() -> adapterSubscriptionInfo.add("(network) completed"))); + } + + private List mapAsList(HashMap map) { + List list = new ArrayList<>(); + + for (String username : map.keySet()) { + String rowLog = String.format("%s [%d]", username, contributionMap.get(username)); + list.add(rowLog); + } + + return list; + } + + private Map dummyDiskData() { + Map map = new HashMap<>(); + map.put("JakeWharton", 0L); + map.put("pforhan", 0L); + map.put("edenman", 0L); + map.put("swankjesse", 0L); + map.put("bruceLee", 0L); + return map; + } +} diff --git a/app/src/main/res/layout/fragment_pseudo_cache.xml b/app/src/main/res/layout/fragment_pseudo_cache.xml new file mode 100644 index 00000000..34ab83f5 --- /dev/null +++ b/app/src/main/res/layout/fragment_pseudo_cache.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + +