diff --git a/.gitignore b/.gitignore index 77e62b9f..74c05caa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,18 +15,25 @@ build/ # Gradle files .gradle/ -build/ subProjects/facebook/build +# Buck files +buck-out/ +.buckd/ + # Intellij project files .idea/ +.idea/libraries +.idea/.name +.idea/compiler.xml +.idea/gradle.xml +.idea/modules.xml +.idea/runConfigurations.xml +.idea/vcs.xml .idea/workspace.xml +.idea/misc.xml gen-external-apklibs/ *.iml - -# Intellij -.idea/ -*.iml *.iws # Local configuration file (sdk path, etc) @@ -43,4 +50,4 @@ pom.xml.* #Ant build.xml ant.properties -local.properties \ No newline at end of file +profiles_settings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 00000000..3682ebc2 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,236 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index bdb5490d..0f796a1a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,45 @@ Learning RxJava for Android by example ============== -This is a repository with real-world useful examples of using RxJava with Android. [It usually will be in a constant state of "Work in Progress" (WIP)](http://nerds.weddingpartyapp.com/tech/2014/09/15/learning-rxjava-with-android-by-example/). +This is a repository with real-world useful examples of using RxJava with Android. [It usually will be in a constant state of "Work in Progress" (WIP)](https://kau.sh/blog/learning-rxjava-with-android-by-example/). -I also gave a talk at a local meetup about warming up to RxJava here. Here's a link to the [video and slides](https://newcircle.com/s/post/1744/2015/06/29/learning-rxjava-for-android-by-example). +I've also been giving talks about Learning Rx using many of the examples listed in this repo. -## Examples: +* [Learning RxJava For Android by Example : Part 1](https://www.youtube.com/watch?v=k3D0cWyNno4) \[[slides](https://speakerdeck.com/kaushikgopal/learning-rxjava-for-android-by-example)\] (SF Android Meetup 2015) +* [Learning Rx by Example : Part 2](https://vimeo.com/190922794) \[[slides](https://speakerdeck.com/kaushikgopal/learning-rx-by-example-2)\] (Øredev 2016) -### Concurrency using schedulers +## Examples: -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. +1. [Background work & concurrency (using Schedulers)](#1-background-work--concurrency-using-schedulers) +2. [Accumulate calls (using buffer)](#2-accumulate-calls-using-buffer) +3. [Instant/Auto searching text listeners (using Subjects & debounce)](#3-instantauto-searching-text-listeners-using-subjects--debounce) +4. [Networking with Retrofit & RxJava (using zip, flatmap)](#4-networking-with-retrofit--rxjava-using-zip-flatmap) +5. [Two-way data binding for TextViews (using PublishSubject)](#5-two-way-data-binding-for-textviews-using-publishsubject) +6. [Simple and Advanced polling (using interval and repeatWhen)](#6-simple-and-advanced-polling-using-interval-and-repeatwhen) +7. [Simple and Advanced exponential backoff (using delay and retryWhen)](#7-simple-and-advanced-exponential-backoff-using-delay-and-retrywhen) +8. [Form validation (using combineLatest)](#8-form-validation-using-combinelatest) +9. [Pseudo caching : retrieve data first from a cache, then a network call (using concat, concatEager, merge or publish)](#9-pseudo-caching--retrieve-data-first-from-a-cache-then-a-network-call-using-concat-concateager-merge-or-publish) +10. [Simple timing demos (using timer, interval or delay)](#10-simple-timing-demos-using-timer-interval-and-delay) +11. [RxBus : event bus using RxJava (using RxRelay (never terminating Subjects) and debouncedBuffer)](#11-rxbus--event-bus-using-rxjava-using-rxrelay-never-terminating-subjects-and-debouncedbuffer) +12. [Persist data on Activity rotations (using Subjects and retained Fragments)](#12-persist-data-on-activity-rotations-using-subjects-and-retained-fragments) +13. [Networking with Volley](#13-networking-with-volley) +14. [Pagination with Rx (using Subjects)](#14-pagination-with-rx-using-subjects) +15. [Orchestrating Observable: make parallel network calls, then combine the result into a single data point (using flatmap & zip)](#15-orchestrating-observable-make-parallel-network-calls-then-combine-the-result-into-a-single-data-point-using-flatmap--zip) +16. [Simple Timeout example (using timeout)](#16-simple-timeout-example-using-timeout) +17. [Setup and teardown resources (using `using`)](#17-setup-and-teardown-resources-using-using) +18. [Multicast playground](#18-multicast-playground) + +## Description + +### 1. Background work & concurrency (using Schedulers) + +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). To really see this example shine. Hit the button multiple times and see how the button click (which is a UI operation) is never blocked because the long operation only runs in the background. -### Accumulate calls (buffer) +### 2. Accumulate calls (using buffer) This is a demo of how events can be accumulated using the "buffer" operation. @@ -25,9 +49,9 @@ If you hit the button once, you'll get a message saying the button was hit once. Note: -If you're looking for a more foolproof solution that accumulates "continuous" taps vs just the number of taps within a time span, look at the [EventBus Demo](https://github.com/kaushikgopal/Android-RxJava/blob/master/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java) where a combo of the `publish` and `buffer` operators is used. For a more detailed explanation, you can also have a look at this [blog post](http://nerds.weddingpartyapp.com/tech/2015/01/05/debouncedbuffer-used-in-rxbus-example/). +If you're looking for a more foolproof solution that accumulates "continuous" taps vs just the number of taps within a time span, look at the [EventBus Demo](https://github.com/kaushikgopal/Android-RxJava/blob/master/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java) where a combo of the `publish` and `buffer` operators is used. For a more detailed explanation, you can also have a look at this [blog post](https://kau.sh/blog/debouncedbuffer-with-rxjava/). -### Instant/Auto searching (subject + debounce) +### 3. Instant/Auto searching text listeners (using Subjects & debounce) This is a demo of how events can be swallowed in a way that only the last one is respected. A typical example of this is instant search result boxes. As you type the word "Bruce Lee", you don't want to execute searches for B, Br, Bru, Bruce, Bruce, Bruce L ... etc. But rather intelligently wait for a couple of moments, make sure the user has finished typing the whole word, and then shoot out a single call for "Bruce Lee". @@ -35,52 +59,63 @@ As you type in the input box, it will not shoot out log messages at every single This is the debounce/throttleWithTimeout method in RxJava. -### Retrofit and RxJava (zip, flatmap) +### 4. Networking with Retrofit & RxJava (using zip, flatmap) [Retrofit from Square](http://square.github.io/retrofit/) is an amazing library that helps with easy networking (even if you haven't made the jump to RxJava just yet, you really should check it out). It works even better with RxJava and these are examples hitting the GitHub API, taken straight up from the android demigod-developer Jake Wharton's talk at Netflix. You can [watch the talk](https://www.youtube.com/watch?v=aEuNBk1b5OE#t=2480) at this link. Incidentally, my motivation to use RxJava was from attending this talk at Netflix. -Since it was a presentation, Jake only put up the most important code snippets in [his slides](https://speakerdeck.com/jakewharton/2014-1). Also he uses Java 8 in them, so I flushed those examples out in ~~good~~ old Java 6. (Note: you're most likely to hit the GitHub API quota pretty fast so send in an OAuth-token as a parameter if you want to keep running these examples often). +(Note: you're most likely to hit the GitHub API quota pretty fast so send in an OAuth-token as a parameter if you want to keep running these examples often). -### Volley Demo +### 5. Two-way data binding for TextViews (using PublishSubject) -[Volley](http://developer.android.com/training/volley/index.html) is another networking library introduced by [Google at IO '13](https://www.youtube.com/watch?v=yhv8l9F44qo). A kind citizen of github contributed this example so we know how to integrate Volley with RxJava. +Auto-updating views are a pretty cool thing. If you've dealt with Angular JS before, they have a pretty nifty concept called "two-way data binding", so when an HTML element is bound to a model/entity object, it constantly "listens" to changes on that entity and auto-updates its state based on the model. Using the technique in this example, you could potentially use a pattern like the [Presentation View Model pattern](http://martinfowler.com/eaaDev/PresentationModel.html) with great ease. +While the example here is pretty rudimentary, the technique used to achieve the double binding using a `Publish Subject` is much more interesting. -### Orchestrating Observables. Make parallel network calls, then combine the result into a single data point (flatmap + zip) +### 6. Simple and Advanced polling (using interval and repeatWhen) -The below ascii diagram expresses the intention of our next example with panache. f1,f2,3,f4,f5 are essentially network calls that when made, give back a result that's needed for a future calculation. +This is an example of polling using RxJava Schedulers. This is useful in cases, where you want to constantly poll a server and possibly get new data. The network call is "simulated" so it forces a delay before return a resultant string. +There are two variants for this: - (flatmap) - f1 ___________________ f3 _______ - (flatmap) | (zip) - f2 ___________________ f4 _______| ___________ final output - \ | - \____________ f5 _______| +1. Simple Polling: say when you want to execute a certain task every 5 seconds +2. Increasing Delayed Polling: say when you want to execute a task first in 1 second, then in 2 seconds, then 3 and so on. -The code for this example has already been written by one Mr.skehlet in the interwebs. Head over to [the gist](https://gist.github.com/skehlet/9418379) for the code. It's written in pure Java (6) so it's pretty comprehensible if you've understood the previous examples. I'll flush it out here again when time permits or I've run out of other compelling examples. +The second example is basically a variant of [Exponential Backoff](https://github.com/kaushikgopal/RxJava-Android-Samples#exponential-backoff). -### Double binding with TextViews +Instead of using a RetryWithDelay, we use a RepeatWithDelay here. To understand the difference between Retry(When) and Repeat(When) I wouuld suggest Dan's [fantastic post on the subject](http://blog.danlew.net/2016/01/25/rxjavas-repeatwhen-and-retrywhen-explained/). -Auto-updating views are a pretty cool thing. If you've dealt with Angular JS before, they have a pretty nifty concept called "two-way data binding", so when an HTML element is bound to a model/entity object, it constantly "listens" to changes on that entity and auto-updates its state based on the model. Using the technique in this example, you could potentially use a pattern like the [Presentation View Model pattern](http://martinfowler.com/eaaDev/PresentationModel.html) with great ease. +An alternative approach to delayed polling without the use of `repeatWhen` would be using chained nested delay observables. See [startExecutingWithExponentialBackoffDelay in the ExponentialBackOffFragment example](https://github.com/kaushikgopal/RxJava-Android-Samples/blob/master/app/src/main/java/com/morihacky/android/rxjava/fragments/ExponentialBackoffFragment.java#L111). -While the example here is pretty rudimentary, the technique used to achieve the double binding using a `Publish Subject` is much more interesting. +### 7. Simple and Advanced exponential backoff (using delay and retryWhen) -### Polling with Schedulers +[Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) is a strategy where based on feedback from a certain output, we alter the rate of a process (usually reducing the number of retries or increasing the wait time before retrying or re-executing a certain process). -This is an example of polling using RxJava Schedulers. This is useful in cases, where you want to constantly poll a server and possibly get new data. The network call is "simulated" so it forces a delay before return a resultant string. +The concept makes more sense with examples. RxJava makes it (relatively) simple to implement such a strategy. My thanks to [Mike](https://twitter.com/m_evans10) for suggesting the idea. + +#### Retry (if error) with exponential backoff + +Say you have a network failure. A sensible strategy would be to NOT keep retrying your network call every 1 second. It would be smart instead (nay... elegant!) to retry with increasing delays. So you try at second 1 to execute the network call, no dice? try after 10 seconds... negatory? try after 20 seconds, no cookie? try after 1 minute. If this thing is still failing, you got to give up on the network yo! -### RxBus - An event bus using RxJava + DebouncedBuffer +We simulate this behaviour using RxJava with the [`retryWhen` operator](http://reactivex.io/documentation/operators/retry.html). + +`RetryWithDelay` code snippet courtesy: + +* http://stackoverflow.com/a/25292833/159825 +* Another excellent implementation via @[sddamico](https://github.com/sddamico) : https://gist.github.com/sddamico/c45d7cdabc41e663bea1 +* This one includes support for jittering, by @[leandrofavarin](https://github.com/leandrofavarin) : http://leandrofavarin.com/exponential-backoff-rxjava-operator-with-jitter + +Also look at the [Polling example](https://github.com/kaushikgopal/RxJava-Android-Samples#polling-with-schedulers) where we use a very similar Exponential backoff mechanism. + +#### "Repeat" with exponential backoff + +Another variant of the exponential backoff strategy is to execute an operation for a given number of times but with delayed intervals. So you execute a certain operation 1 second from now, then you execute it again 10 seconds from now, then you execute the operation 20 seconds from now. After a grand total of 3 times you stop executing. -Have a look at the accompanying blog posts for details on this demo: +Simulating this behavior is actually way more simpler than the prevoius retry mechanism. You can use a variant of the `delay` operator to achieve this. -1. [Implementing an event bus with RxJava](http://nerds.weddingpartyapp.com/tech/2014/12/24/implementing-an-event-bus-with-rxjava-rxbus/) -2. [DebouncedBuffer used for the fancier variant of the demo](http://nerds.weddingpartyapp.com/tech/2014/12/24/secret-bonus-part-debouncedbuffer-used-in-rxbus-example/) -3. [share/publish/refcount](http://nerds.weddingpartyapp.com/tech/2015/01/21/rxjava-share-publish-refcount-and-all-that-jazz/) -### Form validation - using [`.combineLatest`](http://reactivex.io/documentation/operators/combinelatest.html) +### 8. Form validation (using [`.combineLatest`](http://reactivex.io/documentation/operators/combinelatest.html)) -Thanks to Dan Lew for giving me this idea in the [fragmented podcast - episode #5](http://fragmentedpodcast.com/episodes/4/) (around the 4:30 mark). +Thanks to Dan Lew for giving me this idea in the [fragmented podcast - episode #4](http://fragmentedpodcast.com/episodes/4/) (around the 4:30 mark). `.combineLatest` allows you to monitor the state of multiple observables at once compactly at a single location. The example demonstrated shows how you can use `.combineLatest` to validate a basic form. There are 3 primary inputs for this form to be considered "valid" (an email, a password and a number). The form will turn valid (the text below turns blue :P) once all the inputs are valid. If they are not, an error is shown against the invalid inputs. @@ -90,21 +125,33 @@ 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) -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. +### 9. Pseudo caching : retrieve data first from a cache, then a network call (using concat, concatEager, merge or publish) + +We have two source Observables: a disk (fast) cache and a network (fresh) call. Typically the disk Observable is much faster than the network Observable. But in order to demonstrate the working, we've also used a fake "slower" disk cache just to see how the operators behave. + +This is demonstrated using 4 techniques: + +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 + +The 4th technique is probably what you want to use eventually but it's interesting to go through the progression of techniques, to understand why. + +`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. -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. +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. -**Update:** +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. -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. +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. -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. +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](https://kau.sh/blog/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. -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`. +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 old implementation. -### Simple Timing demos using timer/interval/delay +### 10. Simple timing demos (using timer, interval and delay) This is a super simple and straightforward example which shows you how to use RxJava's `timer`, `interval` and `delay` operators to handle a bunch of cases where you want to run a task at specific intervals. Basically say NO to Android `TimerTask`s. @@ -114,50 +161,91 @@ Cases demonstrated here: 2. run a task constantly every 1s (there's a delay of 1s before the first task fires off) 3. run a task constantly every 1s (same as above but there's no delay before the first task fires off) 4. run a task constantly every 3s, but after running it 5 times, terminate automatically +5. run a task A, pause for sometime, then execute Task B, then terminate -### Exponential backoff +### 11. RxBus : event bus using RxJava (using RxRelay (never terminating Subjects) and debouncedBuffer) -[Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) is a strategy where based on feedback from a certain output, we alter the rate of a process (usually reducing the number of retries or increasing the wait time before retrying or re-executing a certain process). +There are accompanying blog posts that do a much better job of explaining the details on this demo: -The concept makes more sense with examples. RxJava makes it (relatively) simple to implement such a strategy. My thanks to [Mike](https://twitter.com/m_evans10) for suggesting the idea. +1. [Implementing an event bus with RxJava](https://kau.sh/blog/implementing-an-event-bus-with-rxjava-rxbus/) +2. [DebouncedBuffer used for the fancier variant of the demo](https://kau.sh/blog/debouncedbuffer-with-rxjava/) +3. [share/publish/refcount](https://kau.sh/blog/rxjava-tip-for-the-day-share-publish-refcount-and-all-that-jazz/) -#### Retry (if error) with exponential backoff +### 12. Persist data on Activity rotations (using Subjects and retained Fragments) -Say you have a network failure. A sensible strategy would be to NOT keep retrying your network call every 1 second. It would be smart instead (nay... elegant!) to retry with increasing delays. So you try at second 1 to execute the network call, no dice? try after 10 seconds... negatory? try after 20 seconds, no cookie? try after 1 minute. If this thing is still failing, you got to give up on the network yo! +A common question that's asked when using RxJava in Android is, "how do i resume the work of an observable if a configuration change occurs (activity rotation, language locale change etc.)?". -We simulate this behaviour using RxJava with the [`retryWhen` operator](http://reactivex.io/documentation/operators/retry.html). +This example shows you one strategy viz. using retained Fragments. I started using retained fragments as "worker fragments" after reading this [fantastic post by Alex Lockwood](http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html) quite sometime back. -`RetryWithDelay` code snippet courtesy: +Hit the start button and rotate the screen to your heart's content; you'll see the observable continue from where it left off. -* http://stackoverflow.com/a/25292833/159825 -* Another excellent implementation via @[sddamico](https://github.com/sddamico) : https://gist.github.com/sddamico/c45d7cdabc41e663bea1 +*There are certain quirks about the "hotness" of the source observable used in this example. Check [my blog post](https://kau.sh/blog/a-note-about-the-warmth-share-operator/) out where I explain the specifics.* -#### "Repeat" with exponential backoff +I have since rewritten this example using an alternative approach. While the [`ConnectedObservable` approach worked](https://github.com/kaushikgopal/RxJava-Android-Samples/blob/master/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1WorkerFragment.java#L20) it enters the lands of "multicasting" which can be tricky (thread-safety, .refcount etc.). Subjects on the other hand are far more simple. You can see it rewritten [using a `Subject` here](https://github.com/kaushikgopal/RxJava-Android-Samples/blob/master/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2WorkerFragment.java#L22). -Another variant of the exponential backoff strategy is to execute an operation for a given number of times but with delayed intervals. So you execute a certain operation 1 second from now, then you execute it again 10 seconds from now, then you execute the operation 20 seconds from now. After a grand total of 3 times you stop executing. +I wrote [another blog post](https://tech.instacart.com/how-to-think-about-subjects-part-1/) on how to think about Subjects where I go into some specifics. -Simulating this behavior is actually way more simpler than the prevoius retry mechanism. You can use a variant of the `delay` operator to achieve this. -### Rotation Persist +### 13. Networking with Volley -A common question that's asked when using RxJava in Android is, "how do i resume the work of an observable if a configuration change occurs (activity rotation, language locale change etc.)?". +[Volley](http://developer.android.com/training/volley/index.html) is another networking library introduced by [Google at IO '13](https://www.youtube.com/watch?v=yhv8l9F44qo). A kind citizen of github contributed this example so we know how to integrate Volley with RxJava. -This example shows you one strategy viz. using retained Fragments. I started using retained fragments as "worker fragments" after reading this [fantastic post by Alex Lockwood](http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html) quite sometime back. +### 14. Pagination with Rx (using Subjects) -Hit the start button and rotate the screen to your heart's content; you'll see the observable continue from where it left off. +I leverage the simple use of a Subject here. Honestly, if you don't have your items coming down via an `Observable` already (like through Retrofit or a network request), there's no good reason to use Rx and complicate things. + +This example basically sends the page number to a Subject, and the subject handles adding the items. Notice the use of `concatMap` and the return of an `Observable` from `_itemsFromNetworkCall`. -*There are certain quirks about the "hotness" of the source observable used in this example. Check [my blog post](http://blog.kaush.co/2015/07/11/a-note-about-the-warmth-share-operator/) out where I explain the specifics.* +For kicks, I've also included a `PaginationAutoFragment` example, this "auto-paginates" without us requiring to hit a button. It should be simple to follow if you got how the previous example works. -## Work in Progress: +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): -Examples that I would like to have here, but haven't found the time yet to flush out. +* [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) +### 15. Orchestrating Observable: make parallel network calls, then combine the result into a single data point (using flatmap & zip) -### Pagination +The below ascii diagram expresses the intention of our next example with panache. f1,f2,f3,f4,f5 are essentially network calls that when made, give back a result that's needed for a future calculation. -a. Simple pagination -b. Optimized pagination + (flatmap) + f1 ___________________ f3 _______ + (flatmap) | (zip) + f2 ___________________ f4 _______| ___________ final output + \ | + \____________ f5 _______| + +The code for this example has already been written by one Mr.skehlet in the interwebs. Head over to [the gist](https://gist.github.com/skehlet/9418379) for the code. It's written in pure Java (6) so it's pretty comprehensible if you've understood the previous examples. I'll flush it out here again when time permits or I've run out of other compelling examples. + +### 16. Simple Timeout example (using timeout) + +This is a simple example demonstrating the use of the `.timeout` operator. Button 1 will complete the task before the timeout constraint, while Button 2 will force a timeout error. + +Notice how we can provide a custom Observable that indicates how to react under a timeout Exception. + +### 17. Setup and teardown resources (using `using`) + +The [operator `using`](http://reactivex.io/documentation/operators/using.html) is relatively less known and notoriously hard to Google. It's a beautiful API that helps to setup a (costly) resource, use it and then dispose off in a clean way. + +The nice thing about this operator is that it provides a mechansim to use potentially costly resources in a tightly scoped manner. using -> setup, use and dispose. Think DB connections (like Realm instances), socket connections, thread locks etc. + +### 18. Multicast Playground + +Multicasting in Rx is like a dark art. Not too many folks know how to pull it off without concern. This example condiers two subscribers (in the forms of buttons) and allows you to add/remove subscribers at different points of time and see how the different operators behave under those circumstances. + +The source observale is a timer (`interval`) observable and the reason this was chosen was to intentionally pick a non-terminating observable, so you can test/confirm if your multicast experiment will leak. + +_I also gave a talk about [Multicasting in detail at 360|Andev](https://speakerdeck.com/kaushikgopal/rx-by-example-volume-3-the-multicast-edition). If you have the inclination and time, I highly suggest watching that talk first (specifically the Multicast operator permutation segment) and then messing around with the example here._ + +## Rx 2.x + +All the examples here have been migrated to use RxJava 2.X. + +* Have a look at [PR #83 to see the diff of changes between RxJava 1 and 2](https://github.com/kaushikgopal/RxJava-Android-Samples/pull/83/files) +* [What's different in Rx 2.x](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0) + +We use [David Karnok's Interop library](https://github.com/akarnokd/RxJava2Interop) in some cases as certain libraries like RxBindings, RxRelays, RxJava-Math etc. have not been ported yet to 2.x. ## Contributing: @@ -165,6 +253,18 @@ I try to ensure the examples are not overly contrived but reflect a real-world u I'm wrapping my head around RxJava too so if you feel there's a better way of doing one of the examples mentioned above, open up an issue explaining how. Even better, send a pull request. + +## Sponsorship (Memory Management/Profiling) + +Rx threading is messy business. To help, this project uses YourKit tools for analysis. + +![Yourkit](https://www.yourkit.com/images/yklogo.png) + + +YourKit supports open source projects with innovative and intelligent tools +for monitoring and profiling Java applications. YourKit is the creator of YourKit Java Profiler. + + ## License Licensed under the Apache License, Version 2.0 (the "License"). diff --git a/app/build.gradle b/app/build.gradle index 8db2ecbf..edbcd31c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,38 +1,78 @@ +buildscript { + repositories { +// mavenCentral() + jcenter() + } + dependencies { + classpath 'me.tatarka:gradle-retrolambda:3.6.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" + } + + // Exclude the lombok version that the android plugin depends on. + configurations.classpath.exclude group: 'com.android.tools.external.lombok' +} + apply plugin: 'com.android.application' +apply plugin: 'me.tatarka.retrolambda' +apply plugin: 'com.f2prateek.javafmt' +apply plugin: 'kotlin-android' dependencies { - compile 'com.android.support:support-v13:23.0.1' + compile 'com.android.support:multidex:1.0.1' + compile "com.android.support:support-v13:${supportLibVersion}" + compile "com.android.support:appcompat-v7:${supportLibVersion}" + compile "com.android.support:recyclerview-v7:${supportLibVersion}" + + compile 'com.github.kaushikgopal:CoreTextUtils:c703fa12b6' + compile "com.jakewharton:butterknife:${butterKnifeVersion}" + kapt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}" + compile 'com.jakewharton.timber:timber:4.5.1' + compile "com.squareup.retrofit2:retrofit:${retrofitVersion}" + compile "com.squareup.retrofit2:converter-gson:${retrofitVersion}" + compile "com.squareup.okhttp3:okhttp:${okhttpVersion}" + compile "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}" + compile 'com.mcxiaoke.volley:library:1.0.19' + + compile "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" + compile "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}" + + compile "android.arch.lifecycle:runtime:${archComponentsVersion}" + compile "android.arch.lifecycle:extensions:${archComponentsVersion}" + kapt "android.arch.lifecycle:compiler:${archComponentsVersion}" + + // ---------------------------------- + // Rx dependencies + + compile 'io.reactivex.rxjava2:rxjava:2.0.7' - compile 'io.reactivex:rxandroid:1.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. - compile 'io.reactivex:rxjava:1.0.14' - compile 'io.reactivex:rxjava-math:1.0.0' - compile 'com.jakewharton.rxbinding:rxbinding:0.2.0' - - compile 'com.jakewharton:butterknife:7.0.1' - compile 'com.jakewharton.timber:timber:2.4.2' - compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3' - compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta3' - compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta3' - compile 'com.squareup.okhttp3:okhttp:3.0.1' - compile 'com.squareup.okhttp3:okhttp-urlconnection:3.0.1' - compile 'com.mcxiaoke.volley:library:1.0.19' + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' + + compile 'com.jakewharton.rx:replaying-share-kotlin:2.0.0' + compile "com.github.akarnokd:rxjava2-extensions:0.16.0" + compile 'com.jakewharton.rxrelay2:rxrelay:2.0.0' + + compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3' + // ---------------------------------- + + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' } android { - compileSdkVersion 23 - buildToolsVersion '23.0.2' + compileSdkVersion sdkVersion + buildToolsVersion buildToolsVrs defaultConfig { applicationId "com.morihacky.android.rxjava" - minSdkVersion 14 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" + minSdkVersion 15 + targetSdkVersion sdkVersion + versionCode 2 + versionName "1.2" + multiDexEnabled true } buildTypes { release { @@ -40,4 +80,14 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + packagingOptions { + pickFirst 'META-INF/rxjava.properties' + } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7466dc56..3b469d7c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,8 @@ + + diff --git a/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java b/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java index 41bc79ae..068a393d 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MainActivity.java @@ -2,56 +2,58 @@ import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AppCompatActivity; import com.morihacky.android.rxjava.fragments.MainFragment; import com.morihacky.android.rxjava.fragments.RotationPersist1WorkerFragment; import com.morihacky.android.rxjava.fragments.RotationPersist2WorkerFragment; import com.morihacky.android.rxjava.rxbus.RxBus; -public class MainActivity - extends FragmentActivity { +public class MainActivity extends AppCompatActivity { - private RxBus _rxBus = null; + private RxBus _rxBus = null; - // This is better done with a DI Library like Dagger - public RxBus getRxBusSingleton() { - if (_rxBus == null) { - _rxBus = new RxBus(); - } + @Override + public void onBackPressed() { + super.onBackPressed(); + _removeWorkerFragments(); + } - return _rxBus; - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - @Override - public void onBackPressed() { - super.onBackPressed(); - _removeWorkerFragments(); + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(android.R.id.content, new MainFragment(), this.toString()) + .commit(); } + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .replace(android.R.id.content, new MainFragment(), this.toString()) - .commit(); - } + // This is better done with a DI Library like Dagger + public RxBus getRxBusSingleton() { + if (_rxBus == null) { + _rxBus = new RxBus(); } - private void _removeWorkerFragments() { - Fragment frag = getSupportFragmentManager()// - .findFragmentByTag(RotationPersist1WorkerFragment.class.getName()); + return _rxBus; + } - if (frag != null) { - getSupportFragmentManager().beginTransaction().remove(frag).commit(); - } + private void _removeWorkerFragments() { + Fragment frag = + getSupportFragmentManager() + .findFragmentByTag(RotationPersist1WorkerFragment.class.getName()); + + if (frag != null) { + getSupportFragmentManager().beginTransaction().remove(frag).commit(); + } - frag = getSupportFragmentManager()// - .findFragmentByTag(RotationPersist2WorkerFragment.class.getName()); + frag = + getSupportFragmentManager() + .findFragmentByTag(RotationPersist2WorkerFragment.class.getName()); - if (frag != null) { - getSupportFragmentManager().beginTransaction().remove(frag).commit(); - } + if (frag != null) { + getSupportFragmentManager().beginTransaction().remove(frag).commit(); } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/MyApp.java b/app/src/main/java/com/morihacky/android/rxjava/MyApp.java index 2a8fdc17..23a90b23 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/MyApp.java +++ b/app/src/main/java/com/morihacky/android/rxjava/MyApp.java @@ -1,36 +1,43 @@ package com.morihacky.android.rxjava; -import android.app.Application; - +import android.support.multidex.MultiDexApplication; import com.morihacky.android.rxjava.volley.MyVolley; import com.squareup.leakcanary.LeakCanary; import com.squareup.leakcanary.RefWatcher; import timber.log.Timber; -public class MyApp - extends Application { +public class MyApp extends MultiDexApplication { - private static MyApp _instance; - private RefWatcher _refWatcher; + private static MyApp _instance; + private RefWatcher _refWatcher; - public static MyApp get() { - return _instance; - } + public static MyApp get() { + return _instance; + } + + public static RefWatcher getRefWatcher() { + return MyApp.get()._refWatcher; + } - public static RefWatcher getRefWatcher() { - return MyApp.get()._refWatcher; + @Override + public void onCreate() { + super.onCreate(); + + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; } - @Override - public void onCreate() { - super.onCreate(); + _instance = (MyApp) getApplicationContext(); + _refWatcher = LeakCanary.install(this); - _instance = (MyApp) getApplicationContext(); - _refWatcher = LeakCanary.install(this); + // for better RxJava debugging + //RxJavaHooks.enableAssemblyTracking(); - // Initialize Volley - MyVolley.init(this); + // Initialize Volley + MyVolley.init(this); - Timber.plant(new Timber.DebugTree()); - } + Timber.plant(new Timber.DebugTree()); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/RxUtils.java b/app/src/main/java/com/morihacky/android/rxjava/RxUtils.java deleted file mode 100644 index 100b9aa0..00000000 --- a/app/src/main/java/com/morihacky/android/rxjava/RxUtils.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.morihacky.android.rxjava; - -import rx.Subscription; -import rx.subscriptions.CompositeSubscription; - -public class RxUtils { - - public static void unsubscribeIfNotNull(Subscription subscription) { - if (subscription != null) { - subscription.unsubscribe(); - } - } - - public static CompositeSubscription getNewCompositeSubIfUnsubscribed(CompositeSubscription subscription) { - if (subscription == null || subscription.isUnsubscribed()) { - return new CompositeSubscription(); - } - - return subscription; - } -} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/BaseFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/BaseFragment.java index 429f8ba0..4ce242f3 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/BaseFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/BaseFragment.java @@ -4,13 +4,12 @@ import com.morihacky.android.rxjava.MyApp; import com.squareup.leakcanary.RefWatcher; -public class BaseFragment - extends Fragment { +public class BaseFragment extends Fragment { - @Override - public void onDestroy() { - super.onDestroy(); - RefWatcher refWatcher = MyApp.getRefWatcher(); - refWatcher.watch(this); - } + @Override + public void onDestroy() { + super.onDestroy(); + RefWatcher refWatcher = MyApp.getRefWatcher(); + refWatcher.watch(this); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/BufferDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/BufferDemoFragment.java index 04df144d..bf51b85c 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/BufferDemoFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/BufferDemoFragment.java @@ -10,8 +10,7 @@ import android.widget.Button; import android.widget.ListView; -import com.jakewharton.rxbinding.view.RxView; -import com.jakewharton.rxbinding.view.ViewClickEvent; +import com.jakewharton.rxbinding2.view.RxView; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.wiring.LogAdapter; @@ -19,138 +18,143 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; -import rx.Observer; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; +import butterknife.Unbinder; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableObserver; import timber.log.Timber; /** * This is a demonstration of the `buffer` Observable. * - * The buffer observable allows taps to be collected only within a time span. So taps outside the + *

The buffer observable allows taps to be collected only within a time span. So taps outside the * 2s limit imposed by buffer will get accumulated in the next log statement. * - * If you're looking for a more foolproof solution that accumulates "continuous" taps vs - * a more dumb solution as show below (i.e. number of taps within a timespan) - * look at {@link com.morihacky.android.rxjava.rxbus.RxBusDemo_Bottom3Fragment} where a combo - * of `publish` and `buffer` is used. + *

If you're looking for a more foolproof solution that accumulates "continuous" taps vs a more + * dumb solution as show below (i.e. number of taps within a timespan) look at {@link + * com.morihacky.android.rxjava.rxbus.RxBusDemo_Bottom3Fragment} where a combo of `publish` and + * `buffer` is used. * - * Also http://nerds.weddingpartyapp.com/tech/2015/01/05/debouncedbuffer-used-in-rxbus-example/ + *

Also http://nerds.weddingpartyapp.com/tech/2015/01/05/debouncedbuffer-used-in-rxbus-example/ * if you're looking for words instead of code */ -public class BufferDemoFragment - extends BaseFragment { - - @Bind(R.id.list_threading_log) ListView _logsList; - @Bind(R.id.btn_start_operation) Button _tapBtn; - - private LogAdapter _adapter; - private List _logs; - - private Subscription _subscription; - - @Override - public void onStart() { - super.onStart(); - _subscription = _getBufferedSubscription(); - } - - @Override - public void onPause() { - super.onPause(); - _subscription.unsubscribe(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_buffer, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - // ----------------------------------------------------------------------------------- - // Main Rx entities - - private Subscription _getBufferedSubscription() { - return RxView.clickEvents(_tapBtn) - .map(new Func1() { - @Override - public Integer call(ViewClickEvent onClickEvent) { - Timber.d("--------- GOT A TAP"); - _log("GOT A TAP"); - return 1; - } - }) - .buffer(2, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - - @Override - public void onCompleted() { - // fyi: you'll never reach here - Timber.d("----- onCompleted"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "--------- Woops on error!"); - _log("Dang error! check your logs"); - } - - @Override - public void onNext(List integers) { - Timber.d("--------- onNext"); - if (integers.size() > 0) { - _log(String.format("%d taps", integers.size())); - } else { - Timber.d("--------- No taps received "); - } - } - }); - } - - // ----------------------------------------------------------------------------------- - // Methods that help wiring up the example (irrelevant to RxJava) - - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); - } - - private void _log(String logMsg) { - - if (_isCurrentlyOnMainThread()) { - _logs.add(0, logMsg + " (main thread) "); - _adapter.clear(); - _adapter.addAll(_logs); - } else { - _logs.add(0, logMsg + " (NOT main thread) "); - - // You can only do below stuff on main thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); +public class BufferDemoFragment extends BaseFragment { + + @BindView(R.id.list_threading_log) + ListView _logsList; + + @BindView(R.id.btn_start_operation) + Button _tapBtn; + + private LogAdapter _adapter; + private List _logs; + + private Disposable _disposable; + private Unbinder unbinder; + + @Override + public void onResume() { + super.onResume(); + _disposable = _getBufferedDisposable(); + } + + @Override + public void onPause() { + super.onPause(); + _disposable.dispose(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_buffer, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + // ----------------------------------------------------------------------------------- + // Main Rx entities + + private Disposable _getBufferedDisposable() { + return RxView.clicks(_tapBtn) + .map( + onClickEvent -> { + Timber.d("--------- GOT A TAP"); + _log("GOT A TAP"); + return 1; + }) + .buffer(2, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith( + new DisposableObserver>() { + + @Override + public void onComplete() { + // fyi: you'll never reach here + Timber.d("----- onCompleted"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "--------- Woops on error!"); + _log("Dang error! check your logs"); + } + + @Override + public void onNext(List integers) { + Timber.d("--------- onNext"); + if (integers.size() > 0) { + _log(String.format("%d taps", integers.size())); + } else { + Timber.d("--------- No taps received "); } + } }); - } + } + + // ----------------------------------------------------------------------------------- + // Methods that help wiring up the example (irrelevant to RxJava) + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) "); + _adapter.clear(); + _adapter.addAll(_logs); + } else { + _logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); } + } - private boolean _isCurrentlyOnMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } + private boolean _isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java index 9d630603..9a9a61ea 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java @@ -11,158 +11,155 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.ProgressBar; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; +import butterknife.Unbinder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; import java.util.List; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; -import rx.schedulers.Schedulers; import timber.log.Timber; -public class ConcurrencyWithSchedulersDemoFragment - extends BaseFragment { - - @Bind(R.id.progress_operation_running) ProgressBar _progress; - @Bind(R.id.list_threading_log) ListView _logsList; - - private LogAdapter _adapter; - private List _logs; - private Subscription _subscription; - - @Override - public void onDestroy() { - super.onDestroy(); - if (_subscription != null) { - _subscription.unsubscribe(); - } - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_concurrency_schedulers, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @OnClick(R.id.btn_start_operation) - public void startLongOperation() { - - _progress.setVisibility(View.VISIBLE); - _log("Button Clicked"); - - _subscription = _getObservable()// - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(_getObserver()); // Observer - } - - private Observable _getObservable() { - return Observable.just(true).map(new Func1() { - @Override - public Boolean call(Boolean aBoolean) { - _log("Within Observable"); - _doSomeLongOperation_thatBlocksCurrentThread(); - return aBoolean; - } - }); - } - - /** - * Observer that handles the result through the 3 important actions: - * - * 1. onCompleted - * 2. onError - * 3. onNext - */ - private Observer _getObserver() { - return new Observer() { - - @Override - public void onCompleted() { - _log("On complete"); - _progress.setVisibility(View.INVISIBLE); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "Error in RxJava Demo concurrency"); - _log(String.format("Boo! Error %s", e.getMessage())); - _progress.setVisibility(View.INVISIBLE); - } - - @Override - public void onNext(Boolean bool) { - _log(String.format("onNext with return value \"%b\"", bool)); - } - }; - } - - // ----------------------------------------------------------------------------------- - // Method that help wiring up the example (irrelevant to RxJava) - - private void _doSomeLongOperation_thatBlocksCurrentThread() { - _log("performing long operation"); - - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - Timber.d("Operation was interrupted"); - } - } - - private void _log(String logMsg) { - - if (_isCurrentlyOnMainThread()) { - _logs.add(0, logMsg + " (main thread) "); - _adapter.clear(); - _adapter.addAll(_logs); - } else { - _logs.add(0, logMsg + " (NOT main thread) "); - - // You can only do below stuff on main thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } +public class ConcurrencyWithSchedulersDemoFragment extends BaseFragment { + + @BindView(R.id.progress_operation_running) + ProgressBar _progress; + + @BindView(R.id.list_threading_log) + ListView _logsList; + + private LogAdapter _adapter; + private List _logs; + private CompositeDisposable _disposables = new CompositeDisposable(); + private Unbinder unbinder; + + @Override + public void onDestroy() { + super.onDestroy(); + unbinder.unbind(); + _disposables.clear(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_concurrency_schedulers, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @OnClick(R.id.btn_start_operation) + public void startLongOperation() { + + _progress.setVisibility(View.VISIBLE); + _log("Button Clicked"); + + DisposableObserver d = _getDisposableObserver(); + + _getObservable() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(d); + + _disposables.add(d); + } + + private Observable _getObservable() { + return Observable.just(true) + .map( + aBoolean -> { + _log("Within Observable"); + _doSomeLongOperation_thatBlocksCurrentThread(); + return aBoolean; }); - } + } + + /** + * Observer that handles the result through the 3 important actions: + * + *

1. onCompleted 2. onError 3. onNext + */ + private DisposableObserver _getDisposableObserver() { + return new DisposableObserver() { + + @Override + public void onComplete() { + _log("On complete"); + _progress.setVisibility(View.INVISIBLE); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "Error in RxJava Demo concurrency"); + _log(String.format("Boo! Error %s", e.getMessage())); + _progress.setVisibility(View.INVISIBLE); + } + + @Override + public void onNext(Boolean bool) { + _log(String.format("onNext with return value \"%b\"", bool)); + } + }; + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private void _doSomeLongOperation_thatBlocksCurrentThread() { + _log("performing long operation"); + + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Timber.d("Operation was interrupted"); } - - private void _setupLogger() { - _logs = new ArrayList(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) "); + _adapter.clear(); + _adapter.addAll(_logs); + } else { + _logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); } + } - private boolean _isCurrentlyOnMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + } + + private boolean _isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } - private class LogAdapter - extends ArrayAdapter { + private class LogAdapter extends ArrayAdapter { - public LogAdapter(Context context, List logs) { - super(context, R.layout.item_log, R.id.item_log, logs); - } + public LogAdapter(Context context, List logs) { + super(context, R.layout.item_log, R.id.item_log, logs); } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/DebounceSearchEmitterFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/DebounceSearchEmitterFragment.java index 4967364f..2d62cff7 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/DebounceSearchEmitterFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/DebounceSearchEmitterFragment.java @@ -12,132 +12,134 @@ import android.widget.EditText; import android.widget.ListView; -import com.jakewharton.rxbinding.widget.RxTextView; -import com.jakewharton.rxbinding.widget.TextViewTextChangeEvent; +import com.jakewharton.rxbinding2.widget.RxTextView; +import com.jakewharton.rxbinding2.widget.TextViewTextChangeEvent; import com.morihacky.android.rxjava.R; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; -import rx.Observer; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; +import butterknife.Unbinder; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.observers.DisposableObserver; import timber.log.Timber; +import static co.kaush.core.util.CoreNullnessUtils.isNotNullOrEmpty; import static java.lang.String.format; -public class DebounceSearchEmitterFragment - extends BaseFragment { - - @Bind(R.id.list_threading_log) ListView _logsList; - @Bind(R.id.input_txt_debounce) EditText _inputSearchText; - - private LogAdapter _adapter; - private List _logs; - - private Subscription _subscription; - - @Override - public void onDestroy() { - super.onDestroy(); - if (_subscription != null) { - _subscription.unsubscribe(); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_debounce, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @OnClick(R.id.clr_debounce) - public void onClearLog() { - _logs = new ArrayList<>(); - _adapter.clear(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - - super.onActivityCreated(savedInstanceState); - _setupLogger(); - - _subscription = RxTextView.textChangeEvents(_inputSearchText)// - .debounce(400, TimeUnit.MILLISECONDS)// default Scheduler is Computation - .observeOn(AndroidSchedulers.mainThread())// - .subscribe(_getSearchObserver()); - } - - // ----------------------------------------------------------------------------------- - // Main Rx entities - - private Observer _getSearchObserver() { - return new Observer() { - @Override - public void onCompleted() { - Timber.d("--------- onComplete"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "--------- Woops on error!"); - _log("Dang error. check your logs"); - } - - @Override - public void onNext(TextViewTextChangeEvent onTextChangeEvent) { - _log(format("Searching for %s", onTextChangeEvent.text().toString())); - } - }; - } - - // ----------------------------------------------------------------------------------- - // Method that help wiring up the example (irrelevant to RxJava) - - private void _setupLogger() { - _logs = new ArrayList(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); +public class DebounceSearchEmitterFragment extends BaseFragment { + + @BindView(R.id.list_threading_log) + ListView _logsList; + + @BindView(R.id.input_txt_debounce) + EditText _inputSearchText; + + private LogAdapter _adapter; + private List _logs; + + private Disposable _disposable; + private Unbinder unbinder; + + @Override + public void onDestroy() { + super.onDestroy(); + _disposable.dispose(); + unbinder.unbind(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_debounce, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @OnClick(R.id.clr_debounce) + public void onClearLog() { + _logs = new ArrayList<>(); + _adapter.clear(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + + super.onActivityCreated(savedInstanceState); + _setupLogger(); + + _disposable = + RxTextView.textChangeEvents(_inputSearchText) + .debounce(400, TimeUnit.MILLISECONDS) // default Scheduler is Computation + .filter(changes -> isNotNullOrEmpty(changes.text().toString())) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith(_getSearchObserver()); + } + + // ----------------------------------------------------------------------------------- + // Main Rx entities + + private DisposableObserver _getSearchObserver() { + return new DisposableObserver() { + @Override + public void onComplete() { + Timber.d("--------- onComplete"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "--------- Woops on error!"); + _log("Dang error. check your logs"); + } + + @Override + public void onNext(TextViewTextChangeEvent onTextChangeEvent) { + _log(format("Searching for %s", onTextChangeEvent.text().toString())); + } + }; + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) "); + _adapter.clear(); + _adapter.addAll(_logs); + } else { + _logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); } + } - private void _log(String logMsg) { - - if (_isCurrentlyOnMainThread()) { - _logs.add(0, logMsg + " (main thread) "); - _adapter.clear(); - _adapter.addAll(_logs); - } else { - _logs.add(0, logMsg + " (NOT main thread) "); - - // You can only do below stuff on main thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); - } - } - - private boolean _isCurrentlyOnMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } + private boolean _isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } - private class LogAdapter - extends ArrayAdapter { + private class LogAdapter extends ArrayAdapter { - public LogAdapter(Context context, List logs) { - super(context, R.layout.item_log, R.id.item_log, logs); - } + public LogAdapter(Context context, List logs) { + super(context, R.layout.item_log, R.id.item_log, logs); } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/DoubleBindingTextViewFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/DoubleBindingTextViewFragment.java index 25b491a4..883f043e 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/DoubleBindingTextViewFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/DoubleBindingTextViewFragment.java @@ -7,70 +7,72 @@ import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; - -import com.morihacky.android.rxjava.R; - -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnTextChanged; -import rx.Subscription; -import rx.functions.Action1; -import rx.subjects.PublishSubject; +import com.morihacky.android.rxjava.R; + +import butterknife.Unbinder; +import io.reactivex.disposables.Disposable; +import io.reactivex.processors.PublishProcessor; import static android.text.TextUtils.isEmpty; -public class DoubleBindingTextViewFragment - extends BaseFragment { +public class DoubleBindingTextViewFragment extends BaseFragment { - @Bind(R.id.double_binding_num1) EditText _number1; - @Bind(R.id.double_binding_num2) EditText _number2; - @Bind(R.id.double_binding_result) TextView _result; + @BindView(R.id.double_binding_num1) + EditText _number1; - Subscription _subscription; - PublishSubject _resultEmitterSubject; + @BindView(R.id.double_binding_num2) + EditText _number2; - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_double_binding_textview, container, false); - ButterKnife.bind(this, layout); + @BindView(R.id.double_binding_result) + TextView _result; - _resultEmitterSubject = PublishSubject.create(); - _subscription = _resultEmitterSubject.asObservable().subscribe(new Action1() { - @Override - public void call(Float aFloat) { - _result.setText(String.valueOf(aFloat)); - } - }); + Disposable _disposable; + PublishProcessor _resultEmitterSubject; + private Unbinder unbinder; - onNumberChanged(); - _number2.requestFocus(); + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_double_binding_textview, container, false); + unbinder = ButterKnife.bind(this, layout); - return layout; - } + _resultEmitterSubject = PublishProcessor.create(); + + _disposable = + _resultEmitterSubject.subscribe( + aFloat -> { + _result.setText(String.valueOf(aFloat)); + }); - @OnTextChanged({ R.id.double_binding_num1, R.id.double_binding_num2 }) - public void onNumberChanged() { - float num1 = 0; - float num2 = 0; + onNumberChanged(); + _number2.requestFocus(); - if (!isEmpty(_number1.getText().toString())) { - num1 = Float.parseFloat(_number1.getText().toString()); - } + return layout; + } - if (!isEmpty(_number2.getText().toString())) { - num2 = Float.parseFloat(_number2.getText().toString()); - } + @OnTextChanged({R.id.double_binding_num1, R.id.double_binding_num2}) + public void onNumberChanged() { + float num1 = 0; + float num2 = 0; - _resultEmitterSubject.onNext(num1 + num2); + if (!isEmpty(_number1.getText().toString())) { + num1 = Float.parseFloat(_number1.getText().toString()); } - @Override - public void onDestroyView() { - super.onDestroyView(); - if (_subscription != null) { - _subscription.unsubscribe(); - } + if (!isEmpty(_number2.getText().toString())) { + num2 = Float.parseFloat(_number2.getText().toString()); } + + _resultEmitterSubject.onNext(num1 + num2); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + _disposable.dispose(); + unbinder.unbind(); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/ExponentialBackoffFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/ExponentialBackoffFragment.java index a9e9a194..775feaef 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/ExponentialBackoffFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/ExponentialBackoffFragment.java @@ -8,228 +8,227 @@ import android.view.ViewGroup; import android.widget.ListView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; -import com.morihacky.android.rxjava.RxUtils; import com.morihacky.android.rxjava.wiring.LogAdapter; +import butterknife.Unbinder; +import hu.akarnokd.rxjava2.math.MathFlowable; +import io.reactivex.Flowable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Function; +import io.reactivex.subscribers.DisposableSubscriber; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observable; -import rx.Observer; -import rx.functions.Action0; -import rx.functions.Func1; -import rx.observables.MathObservable; -import rx.subscriptions.CompositeSubscription; +import org.reactivestreams.Publisher; import timber.log.Timber; import static android.os.Looper.getMainLooper; -public class ExponentialBackoffFragment - extends BaseFragment { - - @Bind(R.id.list_threading_log) ListView _logList; - private LogAdapter _adapter; - private List _logs; +public class ExponentialBackoffFragment extends BaseFragment { + + @BindView(R.id.list_threading_log) + ListView _logList; + + private LogAdapter _adapter; + private CompositeDisposable _disposables = new CompositeDisposable(); + private List _logs; + Unbinder unbinder; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_exponential_backoff, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public void onPause() { + super.onPause(); + + _disposables.clear(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + // ----------------------------------------------------------------------------------- + + @OnClick(R.id.btn_eb_retry) + public void startRetryingWithExponentialBackoffStrategy() { + _logs = new ArrayList<>(); + _adapter.clear(); + + DisposableSubscriber disposableSubscriber = + new DisposableSubscriber() { + @Override + public void onNext(Object aVoid) { + Timber.d("on Next"); + } + + @Override + public void onComplete() { + Timber.d("on Completed"); + } + + @Override + public void onError(Throwable e) { + _log("Error: I give up!"); + } + }; + + Flowable.error(new RuntimeException("testing")) // always fails + .retryWhen(new RetryWithDelay(5, 1000)) // notice this is called only onError (onNext + // values sent are ignored) + .doOnSubscribe(subscription -> _log("Attempting the impossible 5 times in intervals of 1s")) + .subscribe(disposableSubscriber); + + _disposables.add(disposableSubscriber); + } + + @OnClick(R.id.btn_eb_delay) + public void startExecutingWithExponentialBackoffDelay() { + + _logs = new ArrayList<>(); + _adapter.clear(); + + DisposableSubscriber disposableSubscriber = + new DisposableSubscriber() { + @Override + public void onNext(Integer integer) { + Timber.d("executing Task %d [xx:%02d]", integer, _getSecondHand()); + _log(String.format("executing Task %d [xx:%02d]", integer, _getSecondHand())); + } + + @Override + public void onError(Throwable e) { + Timber.d(e, "arrrr. Error"); + _log("Error"); + } + + @Override + public void onComplete() { + Timber.d("onCompleted"); + _log("Completed"); + } + }; + + Flowable.range(1, 4) + .delay( + integer -> { + // Rx-y way of doing the Fibonnaci :P + return MathFlowable.sumInt(Flowable.range(1, integer)) + .flatMap( + targetSecondDelay -> + Flowable.just(integer).delay(targetSecondDelay, TimeUnit.SECONDS)); + }) + .doOnSubscribe( + s -> + _log( + String.format( + "Execute 4 tasks with delay - time now: [xx:%02d]", _getSecondHand()))) + .subscribe(disposableSubscriber); + + _disposables.add(disposableSubscriber); + } + + // ----------------------------------------------------------------------------------- + + private int _getSecondHand() { + long millis = System.currentTimeMillis(); + return (int) + (TimeUnit.MILLISECONDS.toSeconds(millis) + - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))); + } + + // ----------------------------------------------------------------------------------- + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + _logs.add(logMsg); + + // You can only do below stuff on main thread. + new Handler(getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); + } - private CompositeSubscription _subscriptions = new CompositeSubscription(); + // ----------------------------------------------------------------------------------- - @Override - public void onResume() { - super.onResume(); - _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); - } + // CAUTION: + // -------------------------------------- + // THIS notificationHandler class HAS NO BUSINESS BEING non-static + // I ONLY did this cause i wanted access to the `_log` method from inside here + // for the purpose of demonstration. In the real world, make it static and LET IT BE!! - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } + // It's 12am in the morning and i feel lazy dammit !!! - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_exponential_backoff, container, false); - ButterKnife.bind(this, layout); - return layout; - } + //public static class RetryWithDelay + public class RetryWithDelay implements Function, Publisher> { - @Override - public void onPause() { - super.onPause(); + private final int _maxRetries; + private final int _retryDelayMillis; + private int _retryCount; - RxUtils.unsubscribeIfNotNull(_subscriptions); + public RetryWithDelay(final int maxRetries, final int retryDelayMillis) { + _maxRetries = maxRetries; + _retryDelayMillis = retryDelayMillis; + _retryCount = 0; } - // ----------------------------------------------------------------------------------- - - @OnClick(R.id.btn_eb_retry) - public void startRetryingWithExponentialBackoffStrategy() { - _logs = new ArrayList<>(); - _adapter.clear(); - - _subscriptions.add(// - Observable// - .error(new RuntimeException("testing")) // always fails - .retryWhen(new RetryWithDelay(5, 1000))// - .doOnSubscribe(new Action0() { - @Override - public void call() { - _log("Attempting the impossible 5 times in intervals of 1s"); - } - })// - .subscribe(new Observer() { - @Override - public void onCompleted() { - Timber.d("on Completed"); - } - - @Override - public void onError(Throwable e) { - _log("Error: I give up!"); - } - - @Override - public void onNext(Object aVoid) { - Timber.d("on Next"); - } - })); - } + // this is a notificationhandler, all that is cared about here + // is the emission "type" not emission "content" + // only onNext triggers a re-subscription (onError + onComplete kills it) - @OnClick(R.id.btn_eb_delay) - public void startExecutingWithExponentialBackoffDelay() { - - _logs = new ArrayList<>(); - _adapter.clear(); - - _subscriptions.add(// - - Observable.range(1, 4)// - .delay(new Func1>() { - @Override - public Observable call(final Integer integer) { - // Rx-y way of doing the Fibonnaci :P - return MathObservable// - .sumInteger(Observable.range(1, integer)) - .flatMap(new Func1>() { - @Override - public Observable call(Integer targetSecondDelay) { - return Observable.just(integer) - .delay(targetSecondDelay, TimeUnit.SECONDS); - } - }); - } - })// - .doOnSubscribe(new Action0() { - @Override - public void call() { - _log(String.format("Execute 4 tasks with delay - time now: [xx:%02d]", - _getSecondHand())); - } - })// - .subscribe(new Observer() { - @Override - public void onCompleted() { - Timber.d("onCompleted"); - _log("Completed"); - } - - @Override - public void onError(Throwable e) { - Timber.d(e, "arrrr. Error"); - _log("Error"); - } - - @Override - public void onNext(Integer integer) { - Timber.d("executing Task %d [xx:%02d]", integer, _getSecondHand()); - _log(String.format("executing Task %d [xx:%02d]", - integer, - _getSecondHand())); - - } - })); - } + @Override + public Publisher apply(Flowable inputObservable) { - // ----------------------------------------------------------------------------------- + // it is critical to use inputObservable in the chain for the result + // ignoring it and doing your own thing will break the sequence - private int _getSecondHand() { - long millis = System.currentTimeMillis(); - return (int) (TimeUnit.MILLISECONDS.toSeconds(millis) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))); - } + return inputObservable.flatMap( + new Function>() { + @Override + public Publisher apply(Throwable throwable) { + if (++_retryCount < _maxRetries) { - // ----------------------------------------------------------------------------------- + // When this Observable calls onNext, the original + // Observable will be retried (i.e. re-subscribed) - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logList.setAdapter(_adapter); - } + Timber.d("Retrying in %d ms", _retryCount * _retryDelayMillis); + _log(String.format("Retrying in %d ms", _retryCount * _retryDelayMillis)); - private void _log(String logMsg) { - _logs.add(logMsg); + return Flowable.timer(_retryCount * _retryDelayMillis, TimeUnit.MILLISECONDS); + } - // You can only do below stuff on main thread. - new Handler(getMainLooper()).post(new Runnable() { + Timber.d("Argh! i give up"); - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); + // Max retries hit. Pass an error so the chain is forcibly completed + // only onNext triggers a re-subscription (onError + onComplete kills it) + return Flowable.error(throwable); } - }); - } - - // ----------------------------------------------------------------------------------- - - // CAUTION: - // -------------------------------------- - // THIS class HAS NO BUSINESS BEING non-static - // I ONLY did this cause i wanted access to the `_log` method from inside here - // for the purpose of demonstration. In the real world, make it static and LET IT BE!! - - // It's 12am in the morning and i feel lazy dammit !!! - - //public static class RetryWithDelay - public class RetryWithDelay - implements Func1, Observable> { - - private final int _maxRetries; - private final int _retryDelayMillis; - private int _retryCount; - - public RetryWithDelay(final int maxRetries, final int retryDelayMillis) { - _maxRetries = maxRetries; - _retryDelayMillis = retryDelayMillis; - _retryCount = 0; - } - - @Override - public Observable call(Observable attempts) { - return attempts.flatMap(new Func1>() { - @Override - public Observable call(Throwable throwable) { - if (++_retryCount < _maxRetries) { - // When this Observable calls onNext, the original - // Observable will be retried (i.e. re-subscribed). - Timber.d("Retrying in %d ms", _retryCount * _retryDelayMillis); - _log(String.format("Retrying in %d ms", _retryCount * _retryDelayMillis)); - - return Observable.timer(_retryCount * _retryDelayMillis, - TimeUnit.MILLISECONDS); - } - - Timber.d("Argh! i give up"); - // Max retries hit. Just pass the error along. - return Observable.error(throwable); - } - }); - } + }); } -} \ No newline at end of file + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/FormValidationCombineLatestFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/FormValidationCombineLatestFragment.java index 3a655888..efc4c2f8 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/FormValidationCombineLatestFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/FormValidationCombineLatestFragment.java @@ -1,120 +1,123 @@ package com.morihacky.android.rxjava.fragments; +import static android.text.TextUtils.isEmpty; +import static android.util.Patterns.EMAIL_ADDRESS; + import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; - -import com.jakewharton.rxbinding.widget.RxTextView; -import com.morihacky.android.rxjava.R; - -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.functions.Func3; +import butterknife.Unbinder; +import com.jakewharton.rxbinding2.widget.RxTextView; +import com.morihacky.android.rxjava.R; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.subscribers.DisposableSubscriber; import timber.log.Timber; -import static android.text.TextUtils.isEmpty; -import static android.util.Patterns.EMAIL_ADDRESS; - -public class FormValidationCombineLatestFragment - extends BaseFragment { - - @Bind(R.id.btn_demo_form_valid) TextView _btnValidIndicator; - @Bind(R.id.demo_combl_email) EditText _email; - @Bind(R.id.demo_combl_password) EditText _password; - @Bind(R.id.demo_combl_num) EditText _number; - - private Observable _emailChangeObservable; - private Observable _passwordChangeObservable; - private Observable _numberChangeObservable; - - private Subscription _subscription = null; - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_form_validation_comb_latest, - container, - false); - ButterKnife.bind(this, layout); - - _emailChangeObservable = RxTextView.textChanges(_email).skip(1); - _passwordChangeObservable = RxTextView.textChanges(_password).skip(1); - _numberChangeObservable = RxTextView.textChanges(_number).skip(1); - - _combineLatestEvents(); - - return layout; - } - - @Override - public void onPause() { - super.onPause(); - if (_subscription != null) { - _subscription.unsubscribe(); - } - } - - private void _combineLatestEvents() { - _subscription = Observable.combineLatest(_emailChangeObservable, - _passwordChangeObservable, - _numberChangeObservable, - new Func3() { - @Override - public Boolean call(CharSequence newEmail, - CharSequence newPassword, - CharSequence newNumber) { - - boolean emailValid = !isEmpty(newEmail) && - EMAIL_ADDRESS.matcher(newEmail).matches(); - if (!emailValid) { - _email.setError("Invalid Email!"); - } - - boolean passValid = !isEmpty(newPassword) && newPassword.length() > 8; - if (!passValid) { - _password.setError("Invalid Password!"); - } - - boolean numValid = !isEmpty(newNumber); - if (numValid) { - int num = Integer.parseInt(newNumber.toString()); - numValid = num > 0 && num <= 100; - } - if (!numValid) { - _number.setError("Invalid Number!"); - } - - return emailValid && passValid && numValid; - - } - })// - .subscribe(new Observer() { - @Override - public void onCompleted() { - Timber.d("completed"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "there was an error"); - } - - @Override - public void onNext(Boolean formValid) { - if (formValid) { - _btnValidIndicator.setBackgroundColor(getResources().getColor(R.color.blue)); - } else { - _btnValidIndicator.setBackgroundColor(getResources().getColor(R.color.gray)); - } - } - }); - } +public class FormValidationCombineLatestFragment extends BaseFragment { + + @BindView(R.id.btn_demo_form_valid) + TextView _btnValidIndicator; + + @BindView(R.id.demo_combl_email) + EditText _email; + + @BindView(R.id.demo_combl_password) + EditText _password; + + @BindView(R.id.demo_combl_num) + EditText _number; + + private DisposableSubscriber _disposableObserver = null; + private Flowable _emailChangeObservable; + private Flowable _numberChangeObservable; + private Flowable _passwordChangeObservable; + private Unbinder unbinder; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_form_validation_comb_latest, container, false); + unbinder = ButterKnife.bind(this, layout); + + _emailChangeObservable = + RxTextView.textChanges(_email).skip(1).toFlowable(BackpressureStrategy.LATEST); + _passwordChangeObservable = + RxTextView.textChanges(_password).skip(1).toFlowable(BackpressureStrategy.LATEST); + _numberChangeObservable = + RxTextView.textChanges(_number).skip(1).toFlowable(BackpressureStrategy.LATEST); + + _combineLatestEvents(); + + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + _disposableObserver.dispose(); + } + + private void _combineLatestEvents() { + + _disposableObserver = + new DisposableSubscriber() { + @Override + public void onNext(Boolean formValid) { + if (formValid) { + _btnValidIndicator.setBackgroundColor( + ContextCompat.getColor(getContext(), R.color.blue)); + } else { + _btnValidIndicator.setBackgroundColor( + ContextCompat.getColor(getContext(), R.color.gray)); + } + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "there was an error"); + } + + @Override + public void onComplete() { + Timber.d("completed"); + } + }; + + Flowable.combineLatest( + _emailChangeObservable, + _passwordChangeObservable, + _numberChangeObservable, + (newEmail, newPassword, newNumber) -> { + boolean emailValid = !isEmpty(newEmail) && EMAIL_ADDRESS.matcher(newEmail).matches(); + if (!emailValid) { + _email.setError("Invalid Email!"); + } + + boolean passValid = !isEmpty(newPassword) && newPassword.length() > 8; + if (!passValid) { + _password.setError("Invalid Password!"); + } + + boolean numValid = !isEmpty(newNumber); + if (numValid) { + int num = Integer.parseInt(newNumber.toString()); + numValid = num > 0 && num <= 100; + } + if (!numValid) { + _number.setError("Invalid Number!"); + } + + return emailValid && passValid && numValid; + }) + .subscribe(_disposableObserver); + } } 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 fbf68624..75f68fa2 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 @@ -7,93 +7,133 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.morihacky.android.rxjava.R; -import com.morihacky.android.rxjava.rxbus.RxBusDemoFragment; - import butterknife.ButterKnife; import butterknife.OnClick; +import butterknife.Unbinder; +import com.morihacky.android.rxjava.R; +import com.morihacky.android.rxjava.pagination.PaginationAutoFragment; +import com.morihacky.android.rxjava.rxbus.RxBusDemoFragment; import com.morihacky.android.rxjava.volley.VolleyDemoFragment; -public class MainFragment - extends BaseFragment { - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_main, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @OnClick(R.id.btn_demo_schedulers) - void demoConcurrencyWithSchedulers() { - clickedOn(new ConcurrencyWithSchedulersDemoFragment()); - } - - @OnClick(R.id.btn_demo_buffer) - void demoBuffer() { - clickedOn(new BufferDemoFragment()); - } - - @OnClick(R.id.btn_demo_debounce) - void demoThrottling() { - clickedOn(new DebounceSearchEmitterFragment()); - } - - @OnClick(R.id.btn_demo_retrofit) - void demoRetrofitCalls() { - clickedOn(new RetrofitFragment()); - } - - @OnClick(R.id.btn_demo_double_binding_textview) - void demoDoubleBindingWithPublishSubject() { - clickedOn(new DoubleBindingTextViewFragment()); - } - - @OnClick(R.id.btn_demo_rxbus) - void demoRxBus() { - clickedOn(new RxBusDemoFragment()); - } - - @OnClick(R.id.btn_demo_form_validation_combinel) - void formValidation() { - clickedOn(new FormValidationCombineLatestFragment()); - } - - @OnClick(R.id.btn_demo_pseudo_cache) - void pseudoCacheDemo() { - clickedOn(new PseudoCacheMergeFragment()); - } - - @OnClick(R.id.btn_demo_timing) - void demoTimerIntervalDelays() { - clickedOn(new TimingDemoFragment()); - } - - @OnClick(R.id.btn_demo_exponential_backoff) - void demoExponentialBackoff() { - clickedOn(new ExponentialBackoffFragment()); - } - - @OnClick(R.id.btn_demo_rotation_persist) - void demoRotationPersist() { - clickedOn(new RotationPersist2Fragment()); - //clickedOn(new RotationPersist1Fragment()); - } - - @OnClick(R.id.btn_demo_volley) - void demoVolleyRequest() { - clickedOn(new VolleyDemoFragment()); - } - - private void clickedOn(@NonNull Fragment fragment) { - final String tag = fragment.getClass().toString(); - getActivity().getSupportFragmentManager() - .beginTransaction() - .addToBackStack(tag) - .replace(android.R.id.content, fragment, tag) - .commit(); - } +public class MainFragment extends BaseFragment { + + private Unbinder unbinder; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_main, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @OnClick(R.id.btn_demo_schedulers) + void demoConcurrencyWithSchedulers() { + clickedOn(new ConcurrencyWithSchedulersDemoFragment()); + } + + @OnClick(R.id.btn_demo_buffer) + void demoBuffer() { + clickedOn(new BufferDemoFragment()); + } + + @OnClick(R.id.btn_demo_debounce) + void demoThrottling() { + clickedOn(new DebounceSearchEmitterFragment()); + } + + @OnClick(R.id.btn_demo_retrofit) + void demoRetrofitCalls() { + clickedOn(new RetrofitFragment()); + } + + @OnClick(R.id.btn_demo_polling) + void demoPolling() { + clickedOn(new PollingFragment()); + } + + @OnClick(R.id.btn_demo_double_binding_textview) + void demoDoubleBindingWithPublishSubject() { + clickedOn(new DoubleBindingTextViewFragment()); + } + + @OnClick(R.id.btn_demo_rxbus) + void demoRxBus() { + clickedOn(new RxBusDemoFragment()); + } + + @OnClick(R.id.btn_demo_form_validation_combinel) + void formValidation() { + clickedOn(new FormValidationCombineLatestFragment()); + } + + @OnClick(R.id.btn_demo_pseudo_cache) + void pseudoCacheDemo() { + clickedOn(new PseudoCacheFragment()); + } + + @OnClick(R.id.btn_demo_timing) + void demoTimerIntervalDelays() { + clickedOn(new TimingDemoFragment()); + } + + @OnClick(R.id.btn_demo_timeout) + void demoTimeout() { + clickedOn(new TimeoutDemoFragment()); + } + + @OnClick(R.id.btn_demo_exponential_backoff) + void demoExponentialBackoff() { + clickedOn(new ExponentialBackoffFragment()); + } + + @OnClick(R.id.btn_demo_rotation_persist) + void demoRotationPersist() { + clickedOn(new RotationPersist3Fragment()); + // clickedOn(new RotationPersist2Fragment()); + // clickedOn(new RotationPersist1Fragment()); + } + + @OnClick(R.id.btn_demo_pagination) + void demoPaging() { + clickedOn(new PaginationAutoFragment()); + //clickedOn(new PaginationFragment()); + } + + @OnClick(R.id.btn_demo_volley) + void demoVolleyRequest() { + clickedOn(new VolleyDemoFragment()); + } + + @OnClick(R.id.btn_demo_networkDetector) + void demoNetworkDetector() { + clickedOn(new NetworkDetectorFragment()); + } + + @OnClick(R.id.btn_demo_using) + void demoUsing() { + clickedOn(new UsingFragment()); + } + + @OnClick(R.id.btn_demo_multicastPlayground) + void demoMulticastPlayground() { + clickedOn(new MulticastPlaygroundFragment()); + } + + private void clickedOn(@NonNull Fragment fragment) { + final String tag = fragment.getClass().toString(); + getActivity() + .getSupportFragmentManager() + .beginTransaction() + .addToBackStack(tag) + .replace(android.R.id.content, fragment, tag) + .commit(); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/NetworkDetectorFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/NetworkDetectorFragment.java new file mode 100644 index 00000000..af1392d4 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/NetworkDetectorFragment.java @@ -0,0 +1,151 @@ +package com.morihacky.android.rxjava.fragments; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +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 butterknife.BindView; +import butterknife.ButterKnife; +import com.morihacky.android.rxjava.R; + +import butterknife.Unbinder; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.processors.PublishProcessor; +import java.util.ArrayList; +import java.util.List; + +public class NetworkDetectorFragment extends BaseFragment { + + @BindView(R.id.list_threading_log) + ListView logsList; + + private LogAdapter adapter; + private BroadcastReceiver broadcastReceiver; + private List logs; + private Disposable disposable; + private PublishProcessor publishProcessor; + private Unbinder unbinder; + + @Override + public void onDestroy() { + super.onDestroy(); + unbinder.unbind(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_network_detector, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setupLogger(); + } + + @Override + public void onStart() { + super.onStart(); + + publishProcessor = PublishProcessor.create(); + + disposable = + publishProcessor + .startWith(getConnectivityStatus(getActivity())) + .distinctUntilChanged() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + online -> { + if (online) { + log("You are online"); + } else { + log("You are offline"); + } + }); + + listenToNetworkConnectivity(); + } + + @Override + public void onStop() { + super.onStop(); + + disposable.dispose(); + getActivity().unregisterReceiver(broadcastReceiver); + } + + private void listenToNetworkConnectivity() { + + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + publishProcessor.onNext(getConnectivityStatus(context)); + } + }; + + final IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + getActivity().registerReceiver(broadcastReceiver, intentFilter); + } + + private boolean getConnectivityStatus(Context context) { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnected(); + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private void log(String logMsg) { + + if (isCurrentlyOnMainThread()) { + logs.add(0, logMsg + " (main thread) "); + adapter.clear(); + adapter.addAll(logs); + } else { + logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + adapter.clear(); + adapter.addAll(logs); + }); + } + } + + private void setupLogger() { + logs = new ArrayList<>(); + adapter = new LogAdapter(getActivity(), new ArrayList<>()); + logsList.setAdapter(adapter); + } + + private boolean isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } + + private class LogAdapter extends ArrayAdapter { + + public LogAdapter(Context context, List logs) { + super(context, R.layout.item_log, R.id.item_log, logs); + } + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/PollingFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/PollingFragment.java index c0a44571..5eb8b187 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/PollingFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PollingFragment.java @@ -10,130 +10,228 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; +import butterknife.Unbinder; +import io.reactivex.Flowable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Function; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observable; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.schedulers.Schedulers; -import rx.subscriptions.CompositeSubscription; +import org.reactivestreams.Publisher; import timber.log.Timber; -public class PollingFragment - extends BaseFragment { - - public static final int INITIAL_DELAY = 0; - public static final int POLLING_INTERVAL = 1000; - @Bind(R.id.list_threading_log) ListView _logsList; - - private LogAdapter _adapter; - private List _logs; - private CompositeSubscription _subscriptions; - private int _counter = 0; - - @Override - public void onDestroy() { - super.onDestroy(); - _subscriptions.unsubscribe(); +public class PollingFragment extends BaseFragment { + + private static final int INITIAL_DELAY = 0; + private static final int POLLING_INTERVAL = 1000; + private static final int POLL_COUNT = 8; + + @BindView(R.id.list_threading_log) + ListView _logsList; + + private LogAdapter _adapter; + private int _counter = 0; + private CompositeDisposable _disposables; + private List _logs; + private Unbinder unbinder; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + _disposables = new CompositeDisposable(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_polling, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + _disposables.clear(); + unbinder.unbind(); + } + + @OnClick(R.id.btn_start_simple_polling) + public void onStartSimplePollingClicked() { + + final int pollCount = POLL_COUNT; + + Disposable d = + Flowable.interval(INITIAL_DELAY, POLLING_INTERVAL, TimeUnit.MILLISECONDS) + .map(this::_doNetworkCallAndGetStringResult) + .take(pollCount) + .doOnSubscribe( + subscription -> { + _log(String.format("Start simple polling - %s", _counter)); + }) + .subscribe( + taskName -> { + _log( + String.format( + Locale.US, + "Executing polled task [%s] now time : [xx:%02d]", + taskName, + _getSecondHand())); + }); + + _disposables.add(d); + } + + @OnClick(R.id.btn_start_increasingly_delayed_polling) + public void onStartIncreasinglyDelayedPolling() { + _setupLogger(); + + final int pollingInterval = POLLING_INTERVAL; + final int pollCount = POLL_COUNT; + + _log( + String.format( + Locale.US, "Start increasingly delayed polling now time: [xx:%02d]", _getSecondHand())); + + _disposables.add( + Flowable.just(1L) + .repeatWhen(new RepeatWithDelay(pollCount, pollingInterval)) + .subscribe( + o -> + _log( + String.format( + Locale.US, + "Executing polled task now time : [xx:%02d]", + _getSecondHand())), + e -> Timber.d(e, "arrrr. Error"))); + } + + // ----------------------------------------------------------------------------------- + + // CAUTION: + // -------------------------------------- + // THIS notificationHandler class HAS NO BUSINESS BEING non-static + // I ONLY did this cause i wanted access to the `_log` method from inside here + // for the purpose of demonstration. In the real world, make it static and LET IT BE!! + + // It's 12am in the morning and i feel lazy dammit !!! + + private String _doNetworkCallAndGetStringResult(long attempt) { + try { + if (attempt == 4) { + // randomly make one event super long so we test that the repeat logic waits + // and accounts for this. + Thread.sleep(9000); + } else { + Thread.sleep(3000); + } + + } catch (InterruptedException e) { + Timber.d("Operation was interrupted"); } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _subscriptions = new CompositeSubscription(); - _setupLogger(); + _counter++; + + return String.valueOf(_counter); + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private int _getSecondHand() { + long millis = System.currentTimeMillis(); + return (int) + (TimeUnit.MILLISECONDS.toSeconds(millis) + - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))); + } + + private void _log(String logMsg) { + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) "); + _adapter.clear(); + _adapter.addAll(_logs); + } else { + _logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); } + } - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_polling, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @OnClick(R.id.btn_start_simple_polling) - public void onStartSimplePollingClicked() { - _subscriptions.add(Observable.create(new Observable.OnSubscribe() { - @Override - public void call(final Subscriber observer) { - - Schedulers.newThread().createWorker() // - .schedulePeriodically(new Action0() { - @Override - public void call() { - observer.onNext(_doNetworkCallAndGetStringResult()); - } - }, INITIAL_DELAY, POLLING_INTERVAL, TimeUnit.MILLISECONDS); - } - }).take(10).subscribe(new Action1() { - @Override - public void call(String s) { - _log(String.format("String polling - %s", s)); - } - })); - } + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + _counter = 0; + } - // ----------------------------------------------------------------------------------- - // Method that help wiring up the example (irrelevant to RxJava) + private boolean _isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } - private String _doNetworkCallAndGetStringResult() { + //public static class RepeatWithDelay + public class RepeatWithDelay implements Function, Publisher> { - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - Timber.d("Operation was interrupted"); - } - _counter++; + private final int _repeatLimit; + private final int _pollingInterval; + private int _repeatCount = 1; - return String.valueOf(_counter); + RepeatWithDelay(int repeatLimit, int pollingInterval) { + _pollingInterval = pollingInterval; + _repeatLimit = repeatLimit; } - private void _log(String logMsg) { - if (_isCurrentlyOnMainThread()) { - _logs.add(0, logMsg + " (main thread) "); - _adapter.clear(); - _adapter.addAll(_logs); - } else { - _logs.add(0, logMsg + " (NOT main thread) "); - - // You can only do below stuff on main thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); - } - } + // this is a notificationhandler, all we care about is + // the emission "type" not emission "content" + // only onNext triggers a re-subscription - private void _setupLogger() { - _logs = new ArrayList(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); - } + @Override + public Publisher apply(Flowable inputFlowable) throws Exception { + // it is critical to use inputObservable in the chain for the result + // ignoring it and doing your own thing will break the sequence - private boolean _isCurrentlyOnMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); + return inputFlowable.flatMap( + new Function>() { + @Override + public Publisher apply(Object o) throws Exception { + if (_repeatCount >= _repeatLimit) { + // terminate the sequence cause we reached the limit + _log("Completing sequence"); + return Flowable.empty(); + } + + // since we don't get an input + // we store state in this handler to tell us the point of time we're firing + _repeatCount++; + + return Flowable.timer(_repeatCount * _pollingInterval, TimeUnit.MILLISECONDS); + } + }); } + } - private class LogAdapter - extends ArrayAdapter { + private class LogAdapter extends ArrayAdapter { - public LogAdapter(Context context, List logs) { - super(context, R.layout.item_log, R.id.item_log, logs); - } + public LogAdapter(Context context, List logs) { + super(context, R.layout.item_log, R.id.item_log, logs); } -} \ No newline at end of file + } +} 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 deleted file mode 100644 index 7e460a44..00000000 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheConcatFragment.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.morihacky.android.rxjava.fragments; - -import android.os.Bundle; -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 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 rx.Observable; -import rx.Subscriber; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; -import timber.log.Timber; - -public class PseudoCacheConcatFragment - extends BaseFragment { - - @Bind(R.id.log_list) ListView _resultList; - - private Subscription _subscription = null; - private HashMap _contributionMap = null; - private ArrayAdapter _adapter; - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_pseudo_cache_concat, container, false); - ButterKnife.bind(this, layout); - _initializeCache(); - return layout; - } - - @Override - public void onPause() { - super.onPause(); - if (_subscription != null) { - _subscription.unsubscribe(); - } - } - - @OnClick(R.id.btn_start_pseudo_cache) - public void onDemoPseudoCacheClicked() { - _adapter = new ArrayAdapter<>(getActivity(), - R.layout.item_log, - R.id.item_log, - new ArrayList()); - - _resultList.setAdapter(_adapter); - _initializeCache(); - - Observable.concat(_getCachedData(), _getFreshData()) - .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); - _adapter.clear(); - _adapter.addAll(getListStringFromMap()); - } - }); - } - - private List getListStringFromMap() { - List list = new ArrayList<>(); - - for (String username : _contributionMap.keySet()) { - String rowLog = String.format("%s [%d]", username, _contributionMap.get(username)); - list.add(rowLog); - } - - return list; - } - - private Observable _getCachedData() { - - List list = new ArrayList<>(); - - for (String username : _contributionMap.keySet()) { - Contributor c = new Contributor(); - c.login = username; - c.contributions = _contributionMap.get(username); - list.add(c); - } - - return Observable.from(list); - } - - private Observable _getFreshData() { - String githubToken = getResources().getString(R.string.github_oauth_token); - GithubApi githubService = GithubService.createGithubService(githubToken); - return githubService.contributors("square", "retrofit") - .flatMap(new Func1, Observable>() { - @Override - public Observable call(List contributors) { - return Observable.from(contributors); - } - }); - } - - private void _initializeCache() { - _contributionMap = new HashMap<>(); - _contributionMap.put("JakeWharton", 0l); - _contributionMap.put("pforhan", 0l); - _contributionMap.put("edenman", 0l); - _contributionMap.put("swankjesse", 0l); - _contributionMap.put("bruceLee", 0l); - } -} 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..a214ce41 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheFragment.java @@ -0,0 +1,332 @@ +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 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 butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +public class PseudoCacheFragment extends BaseFragment { + + @BindView(R.id.info_pseudoCache_demo) + TextView infoText; + + @BindView(R.id.info_pseudoCache_listSubscription) + ListView listSubscriptionInfo; + + @BindView(R.id.info_pseudoCache_listDtl) + ListView listDetail; + + private ArrayAdapter adapterDetail, adapterSubscriptionInfo; + private HashMap contributionMap = null; + private Unbinder unbinder; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_pseudo_cache, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @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 DisposableObserver() { + @Override + public void onComplete() { + 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(); + + List> observables = new ArrayList<>(2); + observables.add(getSlowCachedDiskData()); + observables.add(getFreshNetworkData()); + + Observable.concatEager(observables) + .subscribeOn(Schedulers.io()) // we want to add a list item at time of subscription + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new DisposableObserver() { + @Override + public void onComplete() { + 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 DisposableObserver() { + @Override + public void onComplete() { + 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 DisposableObserver() { + @Override + public void onComplete() { + 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 DisposableObserver() { + @Override + public void onComplete() { + 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 DisposableObserver() { + @Override + public void onComplete() { + 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.fromIterable(list) // + .doOnSubscribe( + (data) -> + new Handler(Looper.getMainLooper()) // + .post(() -> adapterSubscriptionInfo.add("(disk) cache subscribed"))) // + .doOnComplete( + () -> + 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::fromIterable) + .doOnSubscribe( + (data) -> + new Handler(Looper.getMainLooper()) // + .post(() -> adapterSubscriptionInfo.add("(network) subscribed"))) // + .doOnComplete( + () -> + 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/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java index 777ff3ae..3871437d 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,144 +8,137 @@ 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 rx.Observable; -import rx.Subscriber; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; -import rx.schedulers.Schedulers; -import timber.log.Timber; - -public class PseudoCacheMergeFragment - extends BaseFragment { - - @Bind(R.id.log_list) ListView _resultList; - private ArrayAdapter _adapter; - private HashMap _contributionMap = null; - private HashMap _resultAgeMap = new HashMap<>(); - private Subscription _subscription = null; - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_pseudo_cache_concat, container, false); - ButterKnife.bind(this, layout); - _initializeCache(); - return layout; - } - - @Override - public void onPause() { - super.onPause(); - if (_subscription != null) { - _subscription.unsubscribe(); - } - } - @OnClick(R.id.btn_start_pseudo_cache) - public void onDemoPseudoCacheClicked() { - _adapter = new ArrayAdapter<>(getActivity(), R.layout.item_log, R.id.item_log, new ArrayList()); - - _resultList.setAdapter(_adapter); - _initializeCache(); - - Observable.merge(_getCachedData(), _getFreshData()) - .subscribeOn(Schedulers.io()) - .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(Pair contributorAgePair) { - Contributor contributor = contributorAgePair.first; - - if (_resultAgeMap.containsKey(contributor) && - _resultAgeMap.get(contributor) > contributorAgePair.second) { - return; - } - - _contributionMap.put(contributor.login, contributor.contributions); - _resultAgeMap.put(contributor, contributorAgePair.second); - - _adapter.clear(); - _adapter.addAll(getListStringFromMap()); - } - }); - } - - private List getListStringFromMap() { - List list = new ArrayList<>(); - - for (String username : _contributionMap.keySet()) { - String rowLog = String.format("%s [%d]", username, _contributionMap.get(username)); - list.add(rowLog); - } +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; - return list; +public class PseudoCacheMergeFragment extends BaseFragment { + + @BindView(R.id.log_list) + ListView _resultList; + + private ArrayAdapter _adapter; + private HashMap _contributionMap = null; + private HashMap _resultAgeMap = new HashMap<>(); + private Unbinder unbinder; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_pseudo_cache_concat, container, false); + unbinder = ButterKnife.bind(this, layout); + _initializeCache(); + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @OnClick(R.id.btn_start_pseudo_cache) + public void onDemoPseudoCacheClicked() { + _adapter = + new ArrayAdapter<>(getActivity(), R.layout.item_log, R.id.item_log, new ArrayList<>()); + + _resultList.setAdapter(_adapter); + _initializeCache(); + + Observable.merge(_getCachedData(), _getFreshData()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new DisposableObserver>() { + @Override + public void onComplete() { + Timber.d("done loading all data"); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "arr something went wrong"); + } + + @Override + public void onNext(Pair contributorAgePair) { + Contributor contributor = contributorAgePair.first; + + if (_resultAgeMap.containsKey(contributor) + && _resultAgeMap.get(contributor) > contributorAgePair.second) { + return; + } + + _contributionMap.put(contributor.login, contributor.contributions); + _resultAgeMap.put(contributor, contributorAgePair.second); + + _adapter.clear(); + _adapter.addAll(getListStringFromMap()); + } + }); + } + + private List getListStringFromMap() { + List list = new ArrayList<>(); + + for (String username : _contributionMap.keySet()) { + String rowLog = String.format("%s [%d]", username, _contributionMap.get(username)); + list.add(rowLog); } - private Observable> _getCachedData() { - - List> list = new ArrayList<>(); + return list; + } - Pair dataWithAgePair; + private Observable> _getCachedData() { - for (String username : _contributionMap.keySet()) { - Contributor c = new Contributor(); - c.login = username; - c.contributions = _contributionMap.get(username); + List> list = new ArrayList<>(); - dataWithAgePair = new Pair<>(c, System.currentTimeMillis()); - list.add(dataWithAgePair); - } + Pair dataWithAgePair; - return Observable.from(list); - } + for (String username : _contributionMap.keySet()) { + Contributor c = new Contributor(); + c.login = username; + c.contributions = _contributionMap.get(username); - private Observable> _getFreshData() { - String githubToken = getResources().getString(R.string.github_oauth_token); - GithubApi githubService = GithubService.createGithubService(githubToken); - - return githubService.contributors("square", "retrofit") - .flatMap(new Func1, Observable>() { - @Override - public Observable call(List contributors) { - return Observable.from(contributors); - } - }) - .map(new Func1>() { - @Override - public Pair call(Contributor contributor) { - return new Pair<>(contributor, System.currentTimeMillis()); - } - }); + dataWithAgePair = new Pair<>(c, System.currentTimeMillis()); + list.add(dataWithAgePair); } - private void _initializeCache() { - _contributionMap = new HashMap<>(); - _contributionMap.put("JakeWharton", 0l); - _contributionMap.put("pforhan", 0l); - _contributionMap.put("edenman", 0l); - _contributionMap.put("swankjesse", 0l); - _contributionMap.put("bruceLee", 0l); - } + return Observable.fromIterable(list); + } + + private Observable> _getFreshData() { + String githubToken = getResources().getString(R.string.github_oauth_token); + GithubApi githubService = GithubService.createGithubService(githubToken); + + return githubService + .contributors("square", "retrofit") + .flatMap(Observable::fromIterable) + .map(contributor -> new Pair<>(contributor, System.currentTimeMillis())); + } + + private void _initializeCache() { + _contributionMap = new HashMap<>(); + _contributionMap.put("JakeWharton", 0l); + _contributionMap.put("pforhan", 0l); + _contributionMap.put("edenman", 0l); + _contributionMap.put("swankjesse", 0l); + _contributionMap.put("bruceLee", 0l); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitAsyncTaskDeathFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitAsyncTaskDeathFragment.java index a8ff66a1..baa391ed 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitAsyncTaskDeathFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitAsyncTaskDeathFragment.java @@ -18,95 +18,62 @@ import java.util.ArrayList; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; -import rx.Observer; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; +import butterknife.Unbinder; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; import static java.lang.String.format; -public class RetrofitAsyncTaskDeathFragment - extends Fragment { +public class RetrofitAsyncTaskDeathFragment extends Fragment { - @Bind(R.id.btn_demo_retrofit_async_death_username) EditText _username; - @Bind(R.id.log_list) ListView _resultList; + @BindView(R.id.btn_demo_retrofit_async_death_username) + EditText _username; - private GithubApi _githubService; - private ArrayAdapter _adapter; + @BindView(R.id.log_list) + ListView _resultList; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + private GithubApi _githubService; + private ArrayAdapter _adapter; + private Unbinder unbinder; - String githubToken = getResources().getString(R.string.github_oauth_token); - _githubService = GithubService.createGithubService(githubToken); - } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - - View layout = inflater.inflate(R.layout.fragment_retrofit_async_task_death, - container, - false); - ButterKnife.bind(this, layout); - - _adapter = new ArrayAdapter<>(getActivity(), - R.layout.item_log, - R.id.item_log, - new ArrayList()); - //_adapter.setNotifyOnChange(true); - _resultList.setAdapter(_adapter); - - return layout; - } + String githubToken = getResources().getString(R.string.github_oauth_token); + _githubService = GithubService.createGithubService(githubToken); + } - @OnClick(R.id.btn_demo_retrofit_async_death) - public void onGetGithubUserClicked() { - _adapter.clear(); + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - /*new AsyncTask() { - @Override - protected User doInBackground(String... params) { - return _githubService.getUser(params[0]); - } + View layout = inflater.inflate(R.layout.fragment_retrofit_async_task_death, container, false); + unbinder = ButterKnife.bind(this, layout); - @Override - protected void onPostExecute(User user) { - _adapter.add(format("%s = [%s: %s]", _username.getText(), user.name, user.email)); - } - }.execute(_username.getText().toString());*/ - - _githubService.user(_username.getText().toString()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onCompleted() { - } - - @Override - public void onError(Throwable e) { - } - - @Override - public void onNext(User user) { - _adapter.add(format("%s = [%s: %s]", - _username.getText(), - user.name, - user.email)); - } - }); - } + _adapter = + new ArrayAdapter<>(getActivity(), R.layout.item_log, R.id.item_log, new ArrayList<>()); + //_adapter.setNotifyOnChange(true); + _resultList.setAdapter(_adapter); - // ----------------------------------------------------------------------------------- + return layout; + } - private class GetGithubUser - extends AsyncTask { + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + @OnClick(R.id.btn_demo_retrofit_async_death) + public void onGetGithubUserClicked() { + _adapter.clear(); + + /*new AsyncTask() { @Override protected User doInBackground(String... params) { return _githubService.getUser(params[0]); @@ -116,5 +83,39 @@ protected User doInBackground(String... params) { protected void onPostExecute(User user) { _adapter.add(format("%s = [%s: %s]", _username.getText(), user.name, user.email)); } + }.execute(_username.getText().toString());*/ + + _githubService + .user(_username.getText().toString()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + new DisposableObserver() { + @Override + public void onComplete() {} + + @Override + public void onError(Throwable e) {} + + @Override + public void onNext(User user) { + _adapter.add(format("%s = [%s: %s]", _username.getText(), user.name, user.email)); + } + }); + } + + // ----------------------------------------------------------------------------------- + + private class GetGithubUser extends AsyncTask { + + @Override + protected User doInBackground(String... params) { + return _githubService.getUser(params[0]); + } + + @Override + protected void onPostExecute(User user) { + _adapter.add(format("%s = [%s: %s]", _username.getText(), user.name, user.email)); } + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitFragment.java index daf9a811..0626e5b0 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitFragment.java @@ -10,175 +10,175 @@ import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; + import com.morihacky.android.rxjava.R; -import com.morihacky.android.rxjava.RxUtils; import com.morihacky.android.rxjava.retrofit.Contributor; import com.morihacky.android.rxjava.retrofit.GithubApi; import com.morihacky.android.rxjava.retrofit.GithubService; import com.morihacky.android.rxjava.retrofit.User; + import java.util.ArrayList; import java.util.List; -import rx.Observable; -import rx.Observer; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.schedulers.Schedulers; -import rx.subscriptions.CompositeSubscription; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; import static android.text.TextUtils.isEmpty; import static java.lang.String.format; -public class RetrofitFragment - extends Fragment { - - @Bind(R.id.demo_retrofit_contributors_username) EditText _username; - @Bind(R.id.demo_retrofit_contributors_repository) EditText _repo; - @Bind(R.id.log_list) ListView _resultList; - - private ArrayAdapter _adapter; - private GithubApi _githubService; - private CompositeSubscription _subscriptions = new CompositeSubscription(); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - String githubToken = getResources().getString(R.string.github_oauth_token); - _githubService = GithubService.createGithubService(githubToken); - } - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - - View layout = inflater.inflate(R.layout.fragment_retrofit, container, false); - ButterKnife.bind(this, layout); - - _adapter = new ArrayAdapter<>(getActivity(), R.layout.item_log, R.id.item_log, new ArrayList()); - //_adapter.setNotifyOnChange(true); - _resultList.setAdapter(_adapter); - - return layout; - } - - @Override - public void onResume() { - super.onResume(); - _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); - } - - @Override - public void onPause() { - super.onPause(); - - RxUtils.unsubscribeIfNotNull(_subscriptions); - } - - @OnClick(R.id.btn_demo_retrofit_contributors) - public void onListContributorsClicked() { - _adapter.clear(); - - _subscriptions.add(// - _githubService.contributors(_username.getText().toString(), _repo.getText().toString()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { - @Override - public void onCompleted() { - Timber.d("Retrofit call 1 completed"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "woops we got an error while getting the list of contributors"); - } - - @Override - public void onNext(List contributors) { - for (Contributor c : contributors) { - _adapter.add(format("%s has made %d contributions to %s", - c.login, - c.contributions, - _repo.getText().toString())); - - Timber.d("%s has made %d contributions to %s", - c.login, - c.contributions, - _repo.getText().toString()); - } - } - })); - } - - @OnClick(R.id.btn_demo_retrofit_contributors_with_user_info) - public void onListContributorsWithFullUserInfoClicked() { - _adapter.clear(); - - _subscriptions.add(_githubService.contributors(_username.getText().toString(), _repo.getText().toString()) - .flatMap(new Func1, Observable>() { +public class RetrofitFragment extends Fragment { + + @BindView(R.id.demo_retrofit_contributors_username) + EditText _username; + + @BindView(R.id.demo_retrofit_contributors_repository) + EditText _repo; + + @BindView(R.id.log_list) + ListView _resultList; + + private ArrayAdapter _adapter; + private GithubApi _githubService; + private CompositeDisposable _disposables; + private Unbinder unbinder; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String githubToken = getResources().getString(R.string.github_oauth_token); + _githubService = GithubService.createGithubService(githubToken); + + _disposables = new CompositeDisposable(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + View layout = inflater.inflate(R.layout.fragment_retrofit, container, false); + unbinder = ButterKnife.bind(this, layout); + + _adapter = + new ArrayAdapter<>(getActivity(), R.layout.item_log, R.id.item_log, new ArrayList<>()); + //_adapter.setNotifyOnChange(true); + _resultList.setAdapter(_adapter); + + return layout; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + _disposables.dispose(); + } + + @OnClick(R.id.btn_demo_retrofit_contributors) + public void onListContributorsClicked() { + _adapter.clear(); + + _disposables.add( // + _githubService + .contributors(_username.getText().toString(), _repo.getText().toString()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith( + new DisposableObserver>() { + @Override - public Observable call(List contributors) { - return Observable.from(contributors); + public void onComplete() { + Timber.d("Retrofit call 1 completed"); } - }) - .flatMap(new Func1>>() { + @Override - public Observable> call(Contributor contributor) { - Observable _userObservable = _githubService.user(contributor.login) - .filter(new Func1() { - @Override - public Boolean call(User user) { - return !isEmpty(user.name) && !isEmpty(user.email); - } - }); - - return Observable.zip(_userObservable, - Observable.just(contributor), - new Func2>() { - @Override - public Pair call(User user, Contributor contributor) { - return new Pair<>(user, contributor); - } - }); + public void onError(Throwable e) { + Timber.e(e, "woops we got an error while getting the list of contributors"); + } + + @Override + public void onNext(List contributors) { + for (Contributor c : contributors) { + _adapter.add( + format( + "%s has made %d contributions to %s", + c.login, c.contributions, _repo.getText().toString())); + + Timber.d( + "%s has made %d contributions to %s", + c.login, c.contributions, _repo.getText().toString()); + } } - }) - .subscribeOn(Schedulers.newThread()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer>() { + })); + } + + @OnClick(R.id.btn_demo_retrofit_contributors_with_user_info) + public void onListContributorsWithFullUserInfoClicked() { + _adapter.clear(); + + _disposables.add( + _githubService + .contributors(_username.getText().toString(), _repo.getText().toString()) + .flatMap(Observable::fromIterable) + .flatMap( + contributor -> { + Observable _userObservable = + _githubService + .user(contributor.login) + .filter(user -> !isEmpty(user.name) && !isEmpty(user.email)); + + return Observable.zip(_userObservable, Observable.just(contributor), Pair::new); + }) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith( + new DisposableObserver>() { @Override - public void onCompleted() { - Timber.d("Retrofit call 2 completed "); + public void onComplete() { + Timber.d("Retrofit call 2 completed "); } @Override public void onError(Throwable e) { - Timber.e(e, "error while getting the list of contributors along with full " + "names"); + Timber.e( + e, + "error while getting the list of contributors along with full " + "names"); } @Override public void onNext(Pair pair) { - User user = pair.first; - Contributor contributor = pair.second; + User user = pair.first; + Contributor contributor = pair.second; - _adapter.add(format("%s(%s) has made %d contributions to %s", + _adapter.add( + format( + "%s(%s) has made %d contributions to %s", user.name, user.email, contributor.contributions, _repo.getText().toString())); - _adapter.notifyDataSetChanged(); + _adapter.notifyDataSetChanged(); - Timber.d("%s(%s) has made %d contributions to %s", - user.name, - user.email, - contributor.contributions, - _repo.getText().toString()); + Timber.d( + "%s(%s) has made %d contributions to %s", + user.name, + user.email, + contributor.contributions, + _repo.getText().toString()); } - })); - } + })); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1Fragment.java index 31344672..00882958 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1Fragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1Fragment.java @@ -1,5 +1,7 @@ package com.morihacky.android.rxjava.fragments; +import static android.os.Looper.getMainLooper; + import android.os.Bundle; import android.os.Handler; import android.support.annotation.Nullable; @@ -8,135 +10,129 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; import com.morihacky.android.rxjava.R; -import com.morihacky.android.rxjava.RxUtils; import com.morihacky.android.rxjava.wiring.LogAdapter; - +import io.reactivex.Flowable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subscribers.DisposableSubscriber; import java.util.ArrayList; import java.util.List; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observer; -import rx.functions.Action0; -import rx.observables.ConnectableObservable; -import rx.subscriptions.CompositeSubscription; import timber.log.Timber; -import static android.os.Looper.getMainLooper; - -public class RotationPersist1Fragment - extends BaseFragment - implements RotationPersist1WorkerFragment.IAmYourMaster { - - public static final String FRAG_TAG = RotationPersist1WorkerFragment.class.getName(); - - @Bind(R.id.list_threading_log) ListView _logList; - - private LogAdapter _adapter; - private List _logs; - - private CompositeSubscription _subscriptions = new CompositeSubscription(); +public class RotationPersist1Fragment extends BaseFragment + implements RotationPersist1WorkerFragment.IAmYourMaster { - // ----------------------------------------------------------------------------------- + public static final String TAG = RotationPersist1Fragment.class.toString(); - @OnClick(R.id.btn_rotate_persist) - public void startOperationFromWorkerFrag() { - _logs = new ArrayList<>(); - _adapter.clear(); + @BindView(R.id.list_threading_log) + ListView _logList; - FragmentManager fm = getActivity().getSupportFragmentManager(); - RotationPersist1WorkerFragment frag =// - (RotationPersist1WorkerFragment) fm.findFragmentByTag(FRAG_TAG); + private LogAdapter _adapter; + private List _logs; + private Unbinder unbinder; - if (frag == null) { - frag = new RotationPersist1WorkerFragment(); - fm.beginTransaction().add(frag, FRAG_TAG).commit(); - } else { - Timber.d("Worker frag already spawned"); - } - } - - @Override - public void observeResults(ConnectableObservable intsObservable) { - - _subscriptions.add(// - intsObservable.doOnSubscribe(new Action0() { - @Override - public void call() { - _log("Subscribing to intsObservable"); - } - }).subscribe(new Observer() { - @Override - public void onCompleted() { - _log("Observable is complete"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "Error in worker demo frag observable"); - _log("Dang! something went wrong."); - } - - @Override - public void onNext(Integer integer) { - _log(String.format("Worker frag spits out - %d", integer)); - } - })); - - } + private CompositeDisposable _disposables = new CompositeDisposable(); - // ----------------------------------------------------------------------------------- - // Boilerplate - // ----------------------------------------------------------------------------------- + // ----------------------------------------------------------------------------------- - @Override - public void onResume() { - super.onResume(); - _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } + @OnClick(R.id.btn_rotate_persist) + public void startOperationFromWorkerFrag() { + _logs = new ArrayList<>(); + _adapter.clear(); - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rotation_persist, container, false); - ButterKnife.bind(this, layout); - return layout; - } + FragmentManager fm = getActivity().getSupportFragmentManager(); + RotationPersist1WorkerFragment frag = + (RotationPersist1WorkerFragment) fm.findFragmentByTag(RotationPersist1WorkerFragment.TAG); - @Override - public void onPause() { - super.onPause(); - RxUtils.unsubscribeIfNotNull(_subscriptions); + if (frag == null) { + frag = new RotationPersist1WorkerFragment(); + fm.beginTransaction().add(frag, RotationPersist1WorkerFragment.TAG).commit(); + } else { + Timber.d("Worker frag already spawned"); } - - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logList.setAdapter(_adapter); - } - - private void _log(String logMsg) { - _logs.add(0, logMsg); - - // You can only do below stuff on main thread. - new Handler(getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); - } - -} \ No newline at end of file + } + + @Override + public void observeResults(Flowable intsFlowable) { + + DisposableSubscriber d = + new DisposableSubscriber() { + @Override + public void onNext(Integer integer) { + _log(String.format("Worker frag spits out - %d", integer)); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "Error in worker demo frag observable"); + _log("Dang! something went wrong."); + } + + @Override + public void onComplete() { + _log("Observable is complete"); + } + }; + + intsFlowable + .doOnSubscribe( + subscription -> { + _log("Subscribing to intsObservable"); + }) + .subscribe(d); + + _disposables.add(d); + } + + // ----------------------------------------------------------------------------------- + // Boilerplate + // ----------------------------------------------------------------------------------- + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rotation_persist, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onPause() { + super.onPause(); + _disposables.clear(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + _logs.add(0, logMsg); + + // You can only do below stuff on main thread. + new Handler(getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1WorkerFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1WorkerFragment.java index 58bdede1..d16adc14 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1WorkerFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1WorkerFragment.java @@ -1,108 +1,81 @@ package com.morihacky.android.rxjava.fragments; -import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; import com.morihacky.android.rxjava.MainActivity; -import java.util.List; +import io.reactivex.Flowable; +import io.reactivex.disposables.Disposable; +import io.reactivex.flowables.ConnectableFlowable; import java.util.concurrent.TimeUnit; -import rx.Observable; -import rx.Subscription; -import rx.functions.Func1; -import rx.observables.ConnectableObservable; - -public class RotationPersist1WorkerFragment - extends Fragment { - - private IAmYourMaster _masterFrag; - private ConnectableObservable _storedIntsObservable; - private Subscription _storedIntsSubscription; - - /** - * Hold a reference to the activity -> caller fragment - * this way when the worker frag kicks off - * we can talk back to the master and send results - */ - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - List frags = ((MainActivity) activity).getSupportFragmentManager().getFragments(); - for (Fragment f : frags) { - if (f instanceof IAmYourMaster) { - _masterFrag = (IAmYourMaster) f; - } - } - - if (_masterFrag == null) { - throw new ClassCastException("We did not find a master who can understand us :("); - } - } - /** - * This method will only be called once when the retained Fragment is first created. - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Retain this fragment across configuration changes. - setRetainInstance(true); - - if (_storedIntsObservable != null) { - return; - } - - Observable intsObservable =// - Observable.interval(1, TimeUnit.SECONDS)// - .map(new Func1() { - @Override - public Integer call(Long aLong) { - return aLong.intValue(); - } - })// - .take(20); - - // ----------------------------------------------------------------------------------- - // Making our observable "HOT" for the purpose of the demo. - - //_intsObservable = _intsObservable.share(); - _storedIntsObservable = intsObservable.replay(); - - _storedIntsSubscription = _storedIntsObservable.connect(); - - // Do not do this in production! - // `.share` is "warm" not "hot" - // the below forceful subscription fakes the heat - //_intsObservable.subscribe(); - } +public class RotationPersist1WorkerFragment extends Fragment { - /** - * The Worker fragment has started doing it's thing - */ - @Override - public void onResume() { - super.onResume(); - _masterFrag.observeResults(_storedIntsObservable); - } + public static final String TAG = RotationPersist1WorkerFragment.class.toString(); - /** - * Set the callback to null so we don't accidentally leak the - * Activity instance. - */ - @Override - public void onDetach() { - super.onDetach(); - _masterFrag = null; - } + private IAmYourMaster _masterFrag; + private ConnectableFlowable _storedIntsFlowable; + private Disposable _storedIntsDisposable; + + /** + * Hold a reference to the activity -> caller fragment this way when the worker frag kicks off we + * can talk back to the master and send results + */ + @Override + public void onAttach(Context context) { + super.onAttach(context); - @Override - public void onDestroy() { - super.onDestroy(); - _storedIntsSubscription.unsubscribe(); + _masterFrag = + (RotationPersist1Fragment) + ((MainActivity) context) + .getSupportFragmentManager() + .findFragmentByTag(RotationPersist1Fragment.TAG); + + if (_masterFrag == null) { + throw new ClassCastException("We did not find a master who can understand us :("); } + } + + /** This method will only be called once when the retained Fragment is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - public interface IAmYourMaster { - void observeResults(ConnectableObservable intsObservable); + // Retain this fragment across configuration changes. + setRetainInstance(true); + + if (_storedIntsFlowable != null) { + return; } + + Flowable intsObservable = + Flowable.interval(1, TimeUnit.SECONDS).map(Long::intValue).take(20); + + _storedIntsFlowable = intsObservable.publish(); + _storedIntsDisposable = _storedIntsFlowable.connect(); + } + + /** The Worker fragment has started doing it's thing */ + @Override + public void onResume() { + super.onResume(); + _masterFrag.observeResults(_storedIntsFlowable); + } + + @Override + public void onDestroy() { + super.onDestroy(); + _storedIntsDisposable.dispose(); + } + + /** Set the callback to null so we don't accidentally leak the Activity instance. */ + @Override + public void onDetach() { + super.onDetach(); + _masterFrag = null; + } + + public interface IAmYourMaster { + void observeResults(Flowable intsObservable); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2Fragment.java index 6c84392d..6e523013 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2Fragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2Fragment.java @@ -1,5 +1,7 @@ package com.morihacky.android.rxjava.fragments; +import static android.os.Looper.getMainLooper; + import android.os.Bundle; import android.os.Handler; import android.support.annotation.Nullable; @@ -8,133 +10,115 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; -import com.morihacky.android.rxjava.RxUtils; import com.morihacky.android.rxjava.wiring.LogAdapter; - +import io.reactivex.Flowable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subscribers.DisposableSubscriber; import java.util.ArrayList; import java.util.List; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observable; -import rx.Observer; -import rx.functions.Action0; -import rx.subscriptions.CompositeSubscription; import timber.log.Timber; -import static android.os.Looper.getMainLooper; - -public class RotationPersist2Fragment - extends BaseFragment - implements RotationPersist2WorkerFragment.IAmYourMaster { - - public static final String FRAG_TAG = RotationPersist2WorkerFragment.class.getName(); - - @Bind(R.id.list_threading_log) ListView _logList; - - private LogAdapter _adapter; - private List _logs; +public class RotationPersist2Fragment extends BaseFragment + implements RotationPersist2WorkerFragment.IAmYourMaster { - private CompositeSubscription _subscriptions = new CompositeSubscription(); + public static final String TAG = RotationPersist2Fragment.class.toString(); - // ----------------------------------------------------------------------------------- + @BindView(R.id.list_threading_log) + ListView _logList; - @OnClick(R.id.btn_rotate_persist) - public void startOperationFromWorkerFrag() { - _logs = new ArrayList<>(); - _adapter.clear(); + private LogAdapter _adapter; + private List _logs; - FragmentManager fm = getActivity().getSupportFragmentManager(); - RotationPersist2WorkerFragment frag =// - (RotationPersist2WorkerFragment) fm.findFragmentByTag(FRAG_TAG); + private CompositeDisposable _disposables = new CompositeDisposable(); - if (frag == null) { - frag = new RotationPersist2WorkerFragment(); - fm.beginTransaction().add(frag, FRAG_TAG).commit(); - } else { - Timber.d("Worker frag already spawned"); - } - } - - @Override - public void setStream(Observable intStream) { - - _subscriptions.add(// - intStream.doOnSubscribe(new Action0() { - @Override - public void call() { - _log("Subscribing to intsObservable"); - } - }).subscribe(new Observer() { - @Override - public void onCompleted() { - _log("Observable is complete"); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "Error in worker demo frag observable"); - _log("Dang! something went wrong."); - } - - @Override - public void onNext(Integer integer) { - _log(String.format("Worker frag spits out - %d", integer)); - } - })); - } - - // ----------------------------------------------------------------------------------- - // Boilerplate - // ----------------------------------------------------------------------------------- - - @Override - public void onResume() { - super.onResume(); - _subscriptions = RxUtils.getNewCompositeSubIfUnsubscribed(_subscriptions); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rotation_persist, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @Override - public void onPause() { - super.onPause(); - RxUtils.unsubscribeIfNotNull(_subscriptions); - } - - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logList.setAdapter(_adapter); - } + // ----------------------------------------------------------------------------------- - private void _log(String logMsg) { - _logs.add(0, logMsg); + @OnClick(R.id.btn_rotate_persist) + public void startOperationFromWorkerFrag() { + _logs = new ArrayList<>(); + _adapter.clear(); - // You can only do below stuff on main thread. - new Handler(getMainLooper()).post(new Runnable() { + FragmentManager fm = getActivity().getSupportFragmentManager(); + RotationPersist2WorkerFragment frag = + (RotationPersist2WorkerFragment) fm.findFragmentByTag(RotationPersist2WorkerFragment.TAG); - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); + if (frag == null) { + frag = new RotationPersist2WorkerFragment(); + fm.beginTransaction().add(frag, RotationPersist2WorkerFragment.TAG).commit(); + } else { + Timber.d("Worker frag already spawned"); } -} \ No newline at end of file + } + + @Override + public void setStream(Flowable intStream) { + DisposableSubscriber d = + new DisposableSubscriber() { + @Override + public void onNext(Integer integer) { + _log(String.format("Worker frag spits out - %d", integer)); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "Error in worker demo frag observable"); + _log("Dang! something went wrong."); + } + + @Override + public void onComplete() { + _log("Observable is complete"); + } + }; + + intStream.doOnSubscribe(subscription -> _log("Subscribing to intsObservable")).subscribe(d); + + _disposables.add(d); + } + + // ----------------------------------------------------------------------------------- + // Boilerplate + // ----------------------------------------------------------------------------------- + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rotation_persist, container, false); + ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onPause() { + super.onPause(); + _disposables.clear(); + } + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + _logs.add(0, logMsg); + + // You can only do below stuff on main thread. + new Handler(getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2WorkerFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2WorkerFragment.java index 313d7467..cdbe6cd3 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2WorkerFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2WorkerFragment.java @@ -1,94 +1,80 @@ package com.morihacky.android.rxjava.fragments; -import android.app.Activity; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; import com.morihacky.android.rxjava.MainActivity; -import java.util.List; +import io.reactivex.Flowable; +import io.reactivex.processors.PublishProcessor; import java.util.concurrent.TimeUnit; -import rx.Observable; -import rx.Subscription; -import rx.functions.Func1; -import rx.subjects.PublishSubject; -import rx.subjects.Subject; - -public class RotationPersist2WorkerFragment - extends Fragment { - - private IAmYourMaster _masterFrag; - private Subscription _storedIntsSubscription; - private Subject _intStream = PublishSubject.create(); - - /** - * Since we're holding a reference to the Master a.k.a Activity/Master Frag - * remember to explicitly remove the worker fragment or you'll have a mem leak in your hands. - * - * See {@link MainActivity#onBackPressed()} - */ - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - List frags = ((MainActivity) activity).getSupportFragmentManager().getFragments(); - for (Fragment f : frags) { - if (f instanceof IAmYourMaster) { - _masterFrag = (IAmYourMaster) f; - } - } - - if (_masterFrag == null) { - throw new ClassCastException("We did not find a master who can understand us :("); - } - } - /** - * This method will only be called once when the retained Fragment is first created. - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Retain this fragment across configuration changes. - setRetainInstance(true); - - _storedIntsSubscription =// - Observable.interval(1, TimeUnit.SECONDS)// - .map(new Func1() { - @Override - public Integer call(Long aLong) { - return aLong.intValue(); - } - })// - .take(20)// - .subscribe(_intStream); - } +public class RotationPersist2WorkerFragment extends Fragment { - /** - * The Worker fragment has started doing it's thing - */ - @Override - public void onResume() { - super.onResume(); - _masterFrag.setStream(_intStream.asObservable()); - } + public static final String TAG = RotationPersist2WorkerFragment.class.toString(); - /** - * Set the callback to null so we don't accidentally leak the - * Activity instance. - */ - @Override - public void onDetach() { - super.onDetach(); - _masterFrag = null; - } + private PublishProcessor _intStream; + private PublishProcessor _lifeCycleStream; - @Override - public void onDestroy() { - super.onDestroy(); - _storedIntsSubscription.unsubscribe(); - } + private IAmYourMaster _masterFrag; + + /** + * Since we're holding a reference to the Master a.k.a Activity/Master Frag remember to explicitly + * remove the worker fragment or you'll have a mem leak in your hands. + * + *

See {@link MainActivity#onBackPressed()} + */ + @Override + public void onAttach(Context context) { + super.onAttach(context); - public interface IAmYourMaster { - void setStream(Observable intStream); + _masterFrag = + (RotationPersist2Fragment) + ((MainActivity) context) + .getSupportFragmentManager() + .findFragmentByTag(RotationPersist2Fragment.TAG); + + if (_masterFrag == null) { + throw new ClassCastException("We did not find a master who can understand us :("); } + } + + /** This method will only be called once when the retained Fragment is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + _intStream = PublishProcessor.create(); + _lifeCycleStream = PublishProcessor.create(); + + // Retain this fragment across configuration changes. + setRetainInstance(true); + + _intStream.takeUntil(_lifeCycleStream); + + Flowable.interval(1, TimeUnit.SECONDS).map(Long::intValue).take(20).subscribe(_intStream); + } + + /** The Worker fragment has started doing it's thing */ + @Override + public void onResume() { + super.onResume(); + _masterFrag.setStream(_intStream); + } + + @Override + public void onDestroy() { + super.onDestroy(); + _lifeCycleStream.onComplete(); + } + + /** Set the callback to null so we don't accidentally leak the Activity instance. */ + @Override + public void onDetach() { + super.onDetach(); + _masterFrag = null; + } + + public interface IAmYourMaster { + void setStream(Flowable intStream); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist3Fragment.kt b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist3Fragment.kt new file mode 100644 index 00000000..a69e0426 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist3Fragment.kt @@ -0,0 +1,116 @@ +package com.morihacky.android.rxjava.fragments + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProviders +import android.os.Bundle +import android.os.Handler +import android.os.Looper.getMainLooper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ListView +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.morihacky.android.rxjava.MyApp +import com.morihacky.android.rxjava.R +import com.morihacky.android.rxjava.ext.plus +import com.morihacky.android.rxjava.wiring.LogAdapter +import io.reactivex.Flowable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import timber.log.Timber +import java.util.concurrent.TimeUnit + +class RotationPersist3Fragment : BaseFragment() { + + @BindView(R.id.list_threading_log) + lateinit var logList: ListView + lateinit var adapter: LogAdapter + lateinit var sharedViewModel: SharedViewModel + + private var logs: MutableList = ArrayList() + private var disposables = CompositeDisposable() + + // ----------------------------------------------------------------------------------- + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedViewModel = ViewModelProviders.of(activity).get(SharedViewModel::class.java) + } + + override fun onCreateView( + inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val layout = inflater!!.inflate(R.layout.fragment_rotation_persist, container, false) + ButterKnife.bind(this, layout) + return layout + } + + @OnClick(R.id.btn_rotate_persist) + fun startOperationFromWorkerFrag() { + logs = ArrayList() + adapter.clear() + + disposables += + sharedViewModel + .sourceStream() + .subscribe({ l -> + _log("Received element $l") + }) + } + + // ----------------------------------------------------------------------------------- + // Boilerplate + // ----------------------------------------------------------------------------------- + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + _setupLogger() + } + + override fun onPause() { + super.onPause() + disposables.clear() + } + + private fun _setupLogger() { + logs = ArrayList() + adapter = LogAdapter(activity, ArrayList()) + logList.adapter = adapter + } + + private fun _log(logMsg: String) { + logs.add(0, logMsg) + + // You can only do below stuff on main thread. + Handler(getMainLooper()) + .post { + adapter.clear() + adapter.addAll(logs) + } + } +} + +class SharedViewModel : ViewModel() { + var disposable: Disposable? = null + + var sharedObservable: Flowable = + Flowable.interval(1, TimeUnit.SECONDS) + .take(20) + .doOnNext { l -> Timber.tag("KG").d("onNext $l") } + // .replayingShare() + .replay(1) + .autoConnect(1) { t -> disposable = t } + + fun sourceStream(): Flowable { + return sharedObservable + } + + override fun onCleared() { + super.onCleared() + Timber.tag("KG").d("Clearing ViewModel") + disposable?.dispose() + MyApp.getRefWatcher().watch(this) + } +} + diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/TimeoutDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/TimeoutDemoFragment.java index 1c526595..abeace6b 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/TimeoutDemoFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/TimeoutDemoFragment.java @@ -8,173 +8,176 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.wiring.LogAdapter; - +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.observers.DisposableObserver; +import io.reactivex.schedulers.Schedulers; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observable; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; import timber.log.Timber; -public class TimeoutDemoFragment - extends BaseFragment { +public class TimeoutDemoFragment extends BaseFragment { - @Bind(R.id.list_threading_log) ListView _logsList; + @BindView(R.id.list_threading_log) + ListView _logsList; - private LogAdapter _adapter; - private List _logs; + private LogAdapter _adapter; + private DisposableObserver _disposable; + private List _logs; - private Subscription _subscription; - - @Override - public void onDestroy() { - super.onDestroy(); - _subscription.unsubscribe(); - } + @Override + public void onDestroy() { + super.onDestroy(); - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); + if (_disposable == null) { + return; } - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_subject_timeout, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @OnClick(R.id.btn_demo_timeout_1_2s) - public void onStart2sTask() { - _subscription = _getObservableTask_2sToComplete()// - .observeOn(AndroidSchedulers.mainThread())// - .subscribe(_getEventCompletionObserver()); - } - - @OnClick(R.id.btn_demo_timeout_1_5s) - public void onStart5sTask() { - _subscription = _getObservableFor5sTask()// - .timeout(2, TimeUnit.SECONDS, _getTimeoutObservable()) - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(_getEventCompletionObserver()); - } - - // ----------------------------------------------------------------------------------- - // Main Rx entities - - private Observable _getObservableFor5sTask() { - return Observable.create(new Observable.OnSubscribe() { - - @Override - public void call(Subscriber subscriber) { - _log(String.format("Starting a 5s task")); - subscriber.onNext("5 s"); - try { - Thread.sleep(1200); - } catch (InterruptedException e) { - e.printStackTrace(); - } - subscriber.onCompleted(); + _disposable.dispose(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_subject_timeout, container, false); + ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @OnClick(R.id.btn_demo_timeout_1_2s) + public void onStart2sTask() { + _disposable = _getEventCompletionObserver(); + + _getObservableTask_2sToComplete() + .timeout(3, TimeUnit.SECONDS) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(_disposable); + } + + @OnClick(R.id.btn_demo_timeout_1_5s) + public void onStart5sTask() { + _disposable = _getEventCompletionObserver(); + + _getObservableTask_5sToComplete() + .timeout(3, TimeUnit.SECONDS, _onTimeoutObservable()) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(_disposable); + } + + // ----------------------------------------------------------------------------------- + // Main Rx entities + + private Observable _getObservableTask_5sToComplete() { + return Observable.create( + new ObservableOnSubscribe() { + @Override + public void subscribe(ObservableEmitter subscriber) throws Exception { + _log(String.format("Starting a 5s task")); + subscriber.onNext("5 s"); + try { + Thread.sleep(5_000); + } catch (InterruptedException e) { + e.printStackTrace(); } + subscriber.onComplete(); + } }); - } - - private Observable _getObservableTask_2sToComplete() { - return Observable.create(new Observable.OnSubscribe() { - - @Override - public void call(Subscriber subscriber) { - _log(String.format("Starting a 2s task")); - subscriber.onNext("2 s"); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - subscriber.onCompleted(); - } - }).subscribeOn(Schedulers.computation()).timeout(3, TimeUnit.SECONDS); - } - - private Observable _getTimeoutObservable() { - return Observable.create(new Observable.OnSubscribe() { - - @Override - public void call(Subscriber subscriber) { - _log("Timing out this task ..."); - subscriber.onCompleted(); + } + + private Observable _getObservableTask_2sToComplete() { + return Observable.create( + new ObservableOnSubscribe() { + @Override + public void subscribe(ObservableEmitter subscriber) throws Exception { + _log(String.format("Starting a 2s task")); + subscriber.onNext("2 s"); + try { + Thread.sleep(2_000); + } catch (InterruptedException e) { + e.printStackTrace(); } + subscriber.onComplete(); + } }); - } + } - private Observer _getEventCompletionObserver() { - return new Observer() { - - @Override - public void onCompleted() { - _log(String.format("task was completed")); - } - - @Override - public void onError(Throwable e) { - _log(String.format("Dang a task timeout")); - onCompleted(); - Timber.e(e, "Timeout Demo exception"); - } + private Observable _onTimeoutObservable() { + return Observable.create( + new ObservableOnSubscribe() { - @Override - public void onNext(String taskType) { - _log(String.format("onNext %s task", taskType)); - } - }; - } - - // ----------------------------------------------------------------------------------- - // Method that help wiring up the example (irrelevant to RxJava) - - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); - } - - private void _log(String logMsg) { - - if (_isCurrentlyOnMainThread()) { - _logs.add(0, logMsg + " (main thread) "); - _adapter.clear(); - _adapter.addAll(_logs); - } else { - _logs.add(0, logMsg + " (NOT main thread) "); - - // You can only do below stuff on main thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); - } + @Override + public void subscribe(ObservableEmitter subscriber) throws Exception { + _log("Timing out this task ..."); + subscriber.onError(new Throwable("Timeout Error")); + } + }); + } + + private DisposableObserver _getEventCompletionObserver() { + return new DisposableObserver() { + @Override + public void onNext(String taskType) { + _log(String.format("onNext %s task", taskType)); + } + + @Override + public void onError(Throwable e) { + _log(String.format("Dang a task timeout")); + Timber.e(e, "Timeout Demo exception"); + } + + @Override + public void onComplete() { + _log(String.format("task was completed")); + } + }; + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) "); + _adapter.clear(); + _adapter.addAll(_logs); + } else { + _logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); } + } - private boolean _isCurrentlyOnMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } -} \ No newline at end of file + private boolean _isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/TimingDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/TimingDemoFragment.java index c59605c6..55fb027c 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/TimingDemoFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/TimingDemoFragment.java @@ -7,194 +7,230 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.wiring.LogAdapter; +import butterknife.Unbinder; +import io.reactivex.Flowable; +import io.reactivex.subscribers.DefaultSubscriber; +import io.reactivex.subscribers.DisposableSubscriber; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; - -import butterknife.Bind; -import butterknife.ButterKnife; -import butterknife.OnClick; -import rx.Observable; -import rx.Observer; -import rx.Subscription; import timber.log.Timber; import static android.os.Looper.getMainLooper; import static android.os.Looper.myLooper; -public class TimingDemoFragment - extends BaseFragment { - - @Bind(R.id.list_threading_log) ListView _logsList; - - private LogAdapter _adapter; - private List _logs; - - private Subscription _subscription1 = null; - private Subscription _subscription2 = null; - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_demo_timing, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - // ----------------------------------------------------------------------------------- - - @OnClick(R.id.btn_demo_timing_1) - public void Btn1_RunSingleTaskAfter2s() { - _log(String.format("A1 [%s] --- BTN click", _getCurrentTimestamp())); - - Observable.timer(2, TimeUnit.SECONDS)// - //.just(1).delay(2, TimeUnit.SECONDS)// - .subscribe(new Observer() { - @Override - public void onCompleted() { - _log(String.format("A1 [%s] XXX COMPLETE", _getCurrentTimestamp())); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "something went wrong in TimingDemoFragment example"); - } - - @Override - public void onNext(Long number) { - _log(String.format("A1 [%s] NEXT", _getCurrentTimestamp())); - } - }); - } - - @OnClick(R.id.btn_demo_timing_2) - public void Btn2_RunTask_IntervalOf1s() { - if (_subscription1 != null && !_subscription1.isUnsubscribed()) { - _subscription1.unsubscribe(); - _log(String.format("B2 [%s] XXX BTN KILLED", _getCurrentTimestamp())); - return; - } - - _log(String.format("B2 [%s] --- BTN click", _getCurrentTimestamp())); - - _subscription1 = Observable// - .interval(1, TimeUnit.SECONDS)// - .subscribe(new Observer() { - @Override - public void onCompleted() { - _log(String.format("B2 [%s] XXXX COMPLETE", _getCurrentTimestamp())); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "something went wrong in TimingDemoFragment example"); - } - - @Override - public void onNext(Long number) { - _log(String.format("B2 [%s] NEXT", _getCurrentTimestamp())); - } - }); - } - - @OnClick(R.id.btn_demo_timing_3) - public void Btn3_RunTask_IntervalOf1s_StartImmediately() { - if (_subscription2 != null && !_subscription2.isUnsubscribed()) { - _subscription2.unsubscribe(); - _log(String.format("C3 [%s] XXX BTN KILLED", _getCurrentTimestamp())); - return; - } - - _log(String.format("C3 [%s] --- BTN click", _getCurrentTimestamp())); - - _subscription2 = Observable// - .interval(0, 1, TimeUnit.SECONDS)// - .subscribe(new Observer() { - @Override - public void onCompleted() { - _log(String.format("C3 [%s] XXXX COMPLETE", _getCurrentTimestamp())); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "something went wrong in TimingDemoFragment example"); - } - - @Override - public void onNext(Long number) { - _log(String.format("C3 [%s] NEXT", _getCurrentTimestamp())); - } - }); - } - - @OnClick(R.id.btn_demo_timing_4) - public void Btn4_RunTask5Times_IntervalOf3s() { - _log(String.format("D4 [%s] --- BTN click", _getCurrentTimestamp())); - - Observable// - .interval(3, TimeUnit.SECONDS).take(5)// - .subscribe(new Observer() { - @Override - public void onCompleted() { - _log(String.format("D4 [%s] XXX COMPLETE", _getCurrentTimestamp())); - } - - @Override - public void onError(Throwable e) { - Timber.e(e, "something went wrong in TimingDemoFragment example"); - } - - @Override - public void onNext(Long number) { - _log(String.format("D4 [%s] NEXT", _getCurrentTimestamp())); - } - }); - } - - // ----------------------------------------------------------------------------------- - // Method that help wiring up the example (irrelevant to RxJava) - - @OnClick(R.id.btn_clr) - public void OnClearLog() { - _logs = new ArrayList<>(); - _adapter.clear(); - } - - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); - } - - private void _log(String logMsg) { - _logs.add(0, String.format(logMsg + " [MainThread: %b]", getMainLooper() == myLooper())); - - // You can only do below stuff on main thread. - new Handler(getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); +public class TimingDemoFragment extends BaseFragment { + + @BindView(R.id.list_threading_log) + ListView _logsList; + + private LogAdapter _adapter; + private List _logs; + + private DisposableSubscriber _subscriber1; + private DisposableSubscriber _subscriber2; + private Unbinder unbinder; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_demo_timing, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + // ----------------------------------------------------------------------------------- + + @OnClick(R.id.btn_demo_timing_1) + public void btn1_RunSingleTaskAfter2s() { + _log(String.format("A1 [%s] --- BTN click", _getCurrentTimestamp())); + + Flowable.timer(2, TimeUnit.SECONDS) // + .subscribe( + new DefaultSubscriber() { + @Override + public void onNext(Long number) { + _log(String.format("A1 [%s] NEXT", _getCurrentTimestamp())); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "something went wrong in TimingDemoFragment example"); + } + + @Override + public void onComplete() { + _log(String.format("A1 [%s] XXX COMPLETE", _getCurrentTimestamp())); + } + }); + } + + @OnClick(R.id.btn_demo_timing_2) + public void btn2_RunTask_IntervalOf1s() { + if (_subscriber1 != null && !_subscriber1.isDisposed()) { + _subscriber1.dispose(); + _log(String.format("B2 [%s] XXX BTN KILLED", _getCurrentTimestamp())); + return; } - private String _getCurrentTimestamp() { - return new SimpleDateFormat("k:m:s:S a").format(new Date()); + _log(String.format("B2 [%s] --- BTN click", _getCurrentTimestamp())); + + _subscriber1 = + new DisposableSubscriber() { + @Override + public void onComplete() { + _log(String.format("B2 [%s] XXXX COMPLETE", _getCurrentTimestamp())); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "something went wrong in TimingDemoFragment example"); + } + + @Override + public void onNext(Long number) { + _log(String.format("B2 [%s] NEXT", _getCurrentTimestamp())); + } + }; + + Flowable.interval(1, TimeUnit.SECONDS).subscribe(_subscriber1); + } + + @OnClick(R.id.btn_demo_timing_3) + public void btn3_RunTask_IntervalOf1s_StartImmediately() { + if (_subscriber2 != null && !_subscriber2.isDisposed()) { + _subscriber2.dispose(); + _log(String.format("C3 [%s] XXX BTN KILLED", _getCurrentTimestamp())); + return; } + _log(String.format("C3 [%s] --- BTN click", _getCurrentTimestamp())); + + _subscriber2 = + new DisposableSubscriber() { + @Override + public void onNext(Long number) { + _log(String.format("C3 [%s] NEXT", _getCurrentTimestamp())); + } + + @Override + public void onComplete() { + _log(String.format("C3 [%s] XXXX COMPLETE", _getCurrentTimestamp())); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "something went wrong in TimingDemoFragment example"); + } + }; + + Flowable.interval(0, 1, TimeUnit.SECONDS).subscribe(_subscriber2); + } + + @OnClick(R.id.btn_demo_timing_4) + public void btn4_RunTask5Times_IntervalOf3s() { + _log(String.format("D4 [%s] --- BTN click", _getCurrentTimestamp())); + + Flowable.interval(3, TimeUnit.SECONDS) + .take(5) + .subscribe( + new DefaultSubscriber() { + @Override + public void onNext(Long number) { + _log(String.format("D4 [%s] NEXT", _getCurrentTimestamp())); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "something went wrong in TimingDemoFragment example"); + } + + @Override + public void onComplete() { + _log(String.format("D4 [%s] XXX COMPLETE", _getCurrentTimestamp())); + } + }); + } + + @OnClick(R.id.btn_demo_timing_5) + public void btn5_RunTask5Times_IntervalOf3s() { + _log(String.format("D5 [%s] --- BTN click", _getCurrentTimestamp())); + + Flowable.just("Do task A right away") + .doOnNext(input -> _log(String.format("D5 %s [%s]", input, _getCurrentTimestamp()))) + .delay(1, TimeUnit.SECONDS) + .doOnNext( + oldInput -> + _log( + String.format( + "D5 %s [%s]", "Doing Task B after a delay", _getCurrentTimestamp()))) + .subscribe( + new DefaultSubscriber() { + @Override + public void onComplete() { + _log(String.format("D5 [%s] XXX COMPLETE", _getCurrentTimestamp())); + } + + @Override + public void onError(Throwable e) { + Timber.e(e, "something went wrong in TimingDemoFragment example"); + } + + @Override + public void onNext(String number) { + _log(String.format("D5 [%s] NEXT", _getCurrentTimestamp())); + } + }); + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + @OnClick(R.id.btn_clr) + public void OnClearLog() { + _logs = new ArrayList<>(); + _adapter.clear(); + } + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + _logs.add(0, String.format(logMsg + " [MainThread: %b]", getMainLooper() == myLooper())); + + // You can only do below stuff on main thread. + new Handler(getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); + } + + private String _getCurrentTimestamp() { + return new SimpleDateFormat("k:m:s:S a", Locale.getDefault()).format(new Date()); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAdapter.java b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAdapter.java new file mode 100644 index 00000000..48cc49fd --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAdapter.java @@ -0,0 +1,100 @@ +package com.morihacky.android.rxjava.pagination; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.morihacky.android.rxjava.R; +import com.morihacky.android.rxjava.rxbus.RxBus; + +import java.util.ArrayList; +import java.util.List; + +/** There isn't anything specific to Pagination here. Just wiring for the example */ +class PaginationAdapter extends RecyclerView.Adapter { + + private static final int ITEM_LOG = 0; + private static final int ITEM_BTN = 1; + + private final List _items = new ArrayList<>(); + private final RxBus _bus; + + PaginationAdapter(RxBus bus) { + _bus = bus; + } + + void addItems(List items) { + _items.addAll(items); + } + + @Override + public int getItemViewType(int position) { + if (position == _items.size()) { + return ITEM_BTN; + } + + return ITEM_LOG; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case ITEM_BTN: + return ItemBtnViewHolder.create(parent); + default: + return ItemLogViewHolder.create(parent); + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (getItemViewType(position)) { + case ITEM_LOG: + ((ItemLogViewHolder) holder).bindContent(_items.get(position)); + return; + case ITEM_BTN: + ((ItemBtnViewHolder) holder).bindContent(_bus); + } + } + + @Override + public int getItemCount() { + return _items.size() + 1; // add 1 for paging button + } + + private static class ItemLogViewHolder extends RecyclerView.ViewHolder { + ItemLogViewHolder(View itemView) { + super(itemView); + } + + static ItemLogViewHolder create(ViewGroup parent) { + return new ItemLogViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.item_log, parent, false)); + } + + void bindContent(String content) { + ((TextView) itemView).setText(content); + } + } + + static class ItemBtnViewHolder extends RecyclerView.ViewHolder { + ItemBtnViewHolder(View itemView) { + super(itemView); + } + + static ItemBtnViewHolder create(ViewGroup parent) { + return new ItemBtnViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.item_btn, parent, false)); + } + + void bindContent(RxBus bus) { + ((Button) itemView).setText(R.string.btn_demo_pagination_more); + itemView.setOnClickListener(v -> bus.send(new ItemBtnViewHolder.PageEvent())); + } + + static class PageEvent {} + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAutoAdapter.java b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAutoAdapter.java new file mode 100644 index 00000000..75d8adeb --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAutoAdapter.java @@ -0,0 +1,69 @@ +package com.morihacky.android.rxjava.pagination; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.morihacky.android.rxjava.R; +import com.morihacky.android.rxjava.rxbus.RxBus; +import java.util.ArrayList; +import java.util.List; + +class PaginationAutoAdapter extends RecyclerView.Adapter { + + private static final int ITEM_LOG = 0; + + private final List _items = new ArrayList<>(); + private final RxBus _bus; + + PaginationAutoAdapter(RxBus bus) { + _bus = bus; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return ItemLogViewHolder.create(parent); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ((ItemLogViewHolder) holder).bindContent(_items.get(position)); + + boolean lastPositionReached = position == _items.size() - 1; + if (lastPositionReached) { + _bus.send(new PageEvent()); + } + } + + @Override + public int getItemViewType(int position) { + return ITEM_LOG; + } + + @Override + public int getItemCount() { + return _items.size(); + } + + void addItems(List items) { + _items.addAll(items); + } + + private static class ItemLogViewHolder extends RecyclerView.ViewHolder { + ItemLogViewHolder(View itemView) { + super(itemView); + } + + static ItemLogViewHolder create(ViewGroup parent) { + return new ItemLogViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.item_log, parent, false)); + } + + void bindContent(String content) { + ((TextView) itemView).setText(content); + } + } + + static class PageEvent {} +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAutoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAutoFragment.java new file mode 100644 index 00000000..e90d923b --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationAutoFragment.java @@ -0,0 +1,135 @@ +package com.morihacky.android.rxjava.pagination; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import butterknife.BindView; +import butterknife.ButterKnife; +import com.morihacky.android.rxjava.MainActivity; +import com.morihacky.android.rxjava.R; +import com.morihacky.android.rxjava.fragments.BaseFragment; +import com.morihacky.android.rxjava.rxbus.RxBus; +import io.reactivex.Flowable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.processors.PublishProcessor; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class PaginationAutoFragment extends BaseFragment { + + @BindView(R.id.list_paging) + RecyclerView _pagingList; + + @BindView(R.id.progress_paging) + ProgressBar _progressBar; + + private PaginationAutoAdapter _adapter; + private RxBus _bus; + private CompositeDisposable _disposables; + private PublishProcessor _paginator; + private boolean _requestUnderWay = false; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_pagination, container, false); + ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + _bus = ((MainActivity) getActivity()).getRxBusSingleton(); + + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + _pagingList.setLayoutManager(layoutManager); + + _adapter = new PaginationAutoAdapter(_bus); + _pagingList.setAdapter(_adapter); + + _paginator = PublishProcessor.create(); + } + + @Override + public void onStart() { + super.onStart(); + _disposables = new CompositeDisposable(); + + Disposable d2 = + _paginator + .onBackpressureDrop() + .doOnNext( + i -> { + _requestUnderWay = true; + _progressBar.setVisibility(View.VISIBLE); + }) + .concatMap(this::_itemsFromNetworkCall) + .observeOn(AndroidSchedulers.mainThread()) + .map( + items -> { + _adapter.addItems(items); + _adapter.notifyDataSetChanged(); + + return items; + }) + .doOnNext( + i -> { + _requestUnderWay = false; + _progressBar.setVisibility(View.INVISIBLE); + }) + .subscribe(); + + // I'm using an RxBus purely to hear from a nested button click + // we don't really need Rx for this part. it's just easy ¯\_(ツ)_/¯ + + Disposable d1 = + _bus.asFlowable() + .filter(o -> !_requestUnderWay) + .subscribe( + event -> { + if (event instanceof PaginationAutoAdapter.PageEvent) { + + // trigger the paginator for the next event + int nextPage = _adapter.getItemCount(); + _paginator.onNext(nextPage); + } + }); + + _disposables.add(d1); + _disposables.add(d2); + + _paginator.onNext(0); + } + + @Override + public void onStop() { + super.onStop(); + _disposables.clear(); + } + + /** Fake Observable that simulates a network call and then sends down a list of items */ + private Flowable> _itemsFromNetworkCall(int pageStart) { + return Flowable.just(true) + .observeOn(AndroidSchedulers.mainThread()) + .delay(2, TimeUnit.SECONDS) + .map( + dummy -> { + List items = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + items.add("Item " + (pageStart + i)); + } + return items; + }); + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationFragment.java b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationFragment.java new file mode 100644 index 00000000..bfe727f1 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/pagination/PaginationFragment.java @@ -0,0 +1,128 @@ +package com.morihacky.android.rxjava.pagination; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import butterknife.BindView; +import butterknife.ButterKnife; +import com.morihacky.android.rxjava.MainActivity; +import com.morihacky.android.rxjava.R; +import com.morihacky.android.rxjava.fragments.BaseFragment; +import com.morihacky.android.rxjava.rxbus.RxBus; +import io.reactivex.Flowable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.processors.PublishProcessor; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class PaginationFragment extends BaseFragment { + + @BindView(R.id.list_paging) + RecyclerView _pagingList; + + @BindView(R.id.progress_paging) + ProgressBar _progressBar; + + private PaginationAdapter _adapter; + private RxBus _bus; + private CompositeDisposable _disposables; + private PublishProcessor _paginator; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + _bus = ((MainActivity) getActivity()).getRxBusSingleton(); + + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + _pagingList.setLayoutManager(layoutManager); + + _adapter = new PaginationAdapter(_bus); + _pagingList.setAdapter(_adapter); + + _paginator = PublishProcessor.create(); + } + + @Override + public void onStart() { + super.onStart(); + _disposables = new CompositeDisposable(); + + Disposable d2 = + _paginator + .onBackpressureDrop() + .concatMap(nextPage -> _itemsFromNetworkCall(nextPage + 1, 10)) + .observeOn(AndroidSchedulers.mainThread()) + .map( + items -> { + int start = _adapter.getItemCount() - 1; + + _adapter.addItems(items); + _adapter.notifyItemRangeInserted(start, 10); + + _progressBar.setVisibility(View.INVISIBLE); + + return items; + }) + .subscribe(); + + // I'm using an Rxbus purely to hear from a nested button click + // we don't really need Rx for this part. it's just easy ¯\_(ツ)_/¯ + Disposable d1 = + _bus.asFlowable() + .subscribe( + event -> { + if (event instanceof PaginationAdapter.ItemBtnViewHolder.PageEvent) { + + // trigger the paginator for the next event + int nextPage = _adapter.getItemCount() - 1; + _paginator.onNext(nextPage); + } + }); + + _disposables.add(d1); + _disposables.add(d2); + } + + @Override + public void onStop() { + super.onStop(); + _disposables.clear(); + } + + /** Fake Observable that simulates a network call and then sends down a list of items */ + private Flowable> _itemsFromNetworkCall(int start, int count) { + return Flowable.just(true) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext(dummy -> _progressBar.setVisibility(View.VISIBLE)) + .delay(2, TimeUnit.SECONDS) + .map( + dummy -> { + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add("Item " + (start + i)); + } + return items; + }); + } + + // ----------------------------------------------------------------------------------- + // WIRING up the views required for this example + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_pagination, container, false); + ButterKnife.bind(this, layout); + return layout; + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/retrofit/Contributor.java b/app/src/main/java/com/morihacky/android/rxjava/retrofit/Contributor.java index ad7760fc..5bd61c7a 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/retrofit/Contributor.java +++ b/app/src/main/java/com/morihacky/android/rxjava/retrofit/Contributor.java @@ -1,6 +1,6 @@ package com.morihacky.android.rxjava.retrofit; public class Contributor { - public String login; - public long contributions; + public String login; + public long contributions; } diff --git a/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubApi.java b/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubApi.java index d65cd92a..21e32e8e 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubApi.java +++ b/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubApi.java @@ -2,31 +2,25 @@ import java.util.List; +import io.reactivex.Observable; import retrofit2.http.GET; import retrofit2.http.Path; -import rx.Observable; public interface GithubApi { - /** - * See https://developer.github.com/v3/repos/#list-contributors - */ - @GET("/repos/{owner}/{repo}/contributors") - Observable> contributors(@Path("owner") String owner, - @Path("repo") String repo); + /** See https://developer.github.com/v3/repos/#list-contributors */ + @GET("/repos/{owner}/{repo}/contributors") + Observable> contributors( + @Path("owner") String owner, @Path("repo") String repo); - @GET("/repos/{owner}/{repo}/contributors") - List getContributors(@Path("owner") String owner, @Path("repo") String repo); + @GET("/repos/{owner}/{repo}/contributors") + List getContributors(@Path("owner") String owner, @Path("repo") String repo); - /** - * See https://developer.github.com/v3/users/ - */ - @GET("/users/{user}") - Observable user(@Path("user") String user); + /** See https://developer.github.com/v3/users/ */ + @GET("/users/{user}") + Observable user(@Path("user") String user); - /** - * See https://developer.github.com/v3/users/ - */ - @GET("/users/{user}") - User getUser(@Path("user") String user); -} \ No newline at end of file + /** See https://developer.github.com/v3/users/ */ + @GET("/users/{user}") + User getUser(@Path("user") String user); +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubService.java b/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubService.java index 021faa07..3ab57571 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubService.java +++ b/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubService.java @@ -2,42 +2,45 @@ import android.text.TextUtils; -import java.io.IOException; +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; -import okhttp3.Response; -import retrofit2.GsonConverterFactory; import retrofit2.Retrofit; -import retrofit2.RxJavaCallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; import static java.lang.String.format; public class GithubService { - private GithubService() { } + private GithubService() {} - public static GithubApi createGithubService(final String githubToken) { - Retrofit.Builder builder = new Retrofit.Builder().addCallAdapterFactory(RxJavaCallAdapterFactory.create()) - .addConverterFactory(GsonConverterFactory.create()) - .baseUrl("https://api.github.com"); + public static GithubApi createGithubService(final String githubToken) { + Retrofit.Builder builder = + new Retrofit.Builder() + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl("https://api.github.com"); - if (!TextUtils.isEmpty(githubToken)) { + if (!TextUtils.isEmpty(githubToken)) { - OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() { - @Override public Response intercept(Chain chain) throws IOException { + OkHttpClient client = + new OkHttpClient.Builder() + .addInterceptor( + chain -> { Request request = chain.request(); - Request newReq = request.newBuilder() + Request newReq = + request + .newBuilder() .addHeader("Authorization", format("token %s", githubToken)) .build(); return chain.proceed(newReq); - } - }).build(); + }) + .build(); - builder.client(client); - } - - return builder.build().create(GithubApi.class); + builder.client(client); } + + return builder.build().create(GithubApi.class); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/retrofit/User.java b/app/src/main/java/com/morihacky/android/rxjava/retrofit/User.java index 32fa1043..90f7aa3d 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/retrofit/User.java +++ b/app/src/main/java/com/morihacky/android/rxjava/retrofit/User.java @@ -1,6 +1,6 @@ package com.morihacky.android.rxjava.retrofit; public class User { - public String name; - public String email; + public String name; + public String email; } diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java index 83f61f46..0e8f4fed 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java +++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java @@ -1,30 +1,25 @@ package com.morihacky.android.rxjava.rxbus; -import rx.Observable; -import rx.subjects.PublishSubject; -import rx.subjects.SerializedSubject; -import rx.subjects.Subject; +import com.jakewharton.rxrelay2.PublishRelay; +import com.jakewharton.rxrelay2.Relay; -/** - * courtesy: https://gist.github.com/benjchristensen/04eef9ca0851f3a5d7bf - */ -public class RxBus { +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; - //private final PublishSubject _bus = PublishSubject.create(); +/** courtesy: https://gist.github.com/benjchristensen/04eef9ca0851f3a5d7bf */ +public class RxBus { - // If multiple threads are going to emit events to this - // then it must be made thread-safe like this instead - private final Subject _bus = new SerializedSubject<>(PublishSubject.create()); + private final Relay _bus = PublishRelay.create().toSerialized(); - public void send(Object o) { - _bus.onNext(o); - } + public void send(Object o) { + _bus.accept(o); + } - public Observable toObserverable() { - return _bus; - } + public Flowable asFlowable() { + return _bus.toFlowable(BackpressureStrategy.LATEST); + } - public boolean hasObservers() { - return _bus.hasObservers(); - } -} \ No newline at end of file + public boolean hasObservers() { + return _bus.hasObservers(); + } +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java index 8eac88b7..f08ea85c 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java @@ -9,31 +9,29 @@ import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.fragments.BaseFragment; -public class RxBusDemoFragment - extends BaseFragment { +public class RxBusDemoFragment extends BaseFragment { - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rxbus_demo, container, false); - ButterKnife.bind(this, layout); - return layout; - } + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rxbus_demo, container, false); + ButterKnife.bind(this, layout); + return layout; + } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - getActivity().getSupportFragmentManager() - .beginTransaction() - .replace(R.id.demo_rxbus_frag_1, - new RxBusDemo_TopFragment()).replace(R.id.demo_rxbus_frag_2, - new RxBusDemo_Bottom3Fragment()) - //.replace(R.id.demo_rxbus_frag_2, new RxBusDemo_Bottom2Fragment()) - //.replace(R.id.demo_rxbus_frag_2, new RxBusDemo_Bottom1Fragment()) - .commit(); - } + getActivity() + .getSupportFragmentManager() + .beginTransaction() + .replace(R.id.demo_rxbus_frag_1, new RxBusDemo_TopFragment()) + .replace(R.id.demo_rxbus_frag_2, new RxBusDemo_Bottom3Fragment()) + //.replace(R.id.demo_rxbus_frag_2, new RxBusDemo_Bottom2Fragment()) + //.replace(R.id.demo_rxbus_frag_2, new RxBusDemo_Bottom1Fragment()) + .commit(); + } - public static class TapEvent {} -} \ No newline at end of file + public static class TapEvent {} +} diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java index 975f8ce7..1eb1e2ec 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java @@ -7,64 +7,60 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.morihacky.android.rxjava.MainActivity; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.fragments.BaseFragment; +import io.reactivex.disposables.CompositeDisposable; -import butterknife.Bind; -import butterknife.ButterKnife; -import rx.functions.Action1; -import rx.subscriptions.CompositeSubscription; +public class RxBusDemo_Bottom1Fragment extends BaseFragment { -public class RxBusDemo_Bottom1Fragment - extends BaseFragment { + @BindView(R.id.demo_rxbus_tap_txt) + TextView _tapEventTxtShow; - @Bind(R.id.demo_rxbus_tap_txt) TextView _tapEventTxtShow; - private RxBus _rxBus; - private CompositeSubscription _subscriptions; + private CompositeDisposable _disposables; + private RxBus _rxBus; - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false); - ButterKnife.bind(this, layout); - return layout; - } + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false); + ButterKnife.bind(this, layout); + return layout; + } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); - } + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); + } - @Override - public void onStart() { - super.onStart(); - _subscriptions = new CompositeSubscription(); + @Override + public void onStart() { + super.onStart(); + _disposables = new CompositeDisposable(); - _subscriptions// - .add(_rxBus.toObserverable()// - .subscribe(new Action1() { - @Override - public void call(Object event) { - if (event instanceof RxBusDemoFragment.TapEvent) { - _showTapText(); - } - } - })); - } + _disposables.add( + _rxBus + .asFlowable() + .subscribe( + event -> { + if (event instanceof RxBusDemoFragment.TapEvent) { + _showTapText(); + } + })); + } - @Override - public void onStop() { - super.onStop(); - _subscriptions.unsubscribe(); - } + @Override + public void onStop() { + super.onStop(); + _disposables.clear(); + } - private void _showTapText() { - _tapEventTxtShow.setVisibility(View.VISIBLE); - _tapEventTxtShow.setAlpha(1f); - ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400); - } + private void _showTapText() { + _tapEventTxtShow.setVisibility(View.VISIBLE); + _tapEventTxtShow.setAlpha(1f); + ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java index b84a6328..7cb08f4d 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java @@ -7,99 +7,93 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.morihacky.android.rxjava.MainActivity; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.fragments.BaseFragment; - +import io.reactivex.Flowable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; import java.util.List; import java.util.concurrent.TimeUnit; -import butterknife.Bind; -import butterknife.ButterKnife; -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Action1; -import rx.subscriptions.CompositeSubscription; - -public class RxBusDemo_Bottom2Fragment - extends BaseFragment { - - @Bind(R.id.demo_rxbus_tap_txt) TextView _tapEventTxtShow; - @Bind(R.id.demo_rxbus_tap_count) TextView _tapEventCountShow; - private RxBus _rxBus; - private CompositeSubscription _subscriptions; - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); - } - - @Override - public void onStart() { - super.onStart(); - _subscriptions = new CompositeSubscription(); - - Observable tapEventEmitter = _rxBus.toObserverable().share(); - - _subscriptions// - .add(tapEventEmitter.subscribe(new Action1() { - @Override - public void call(Object event) { - if (event instanceof RxBusDemoFragment.TapEvent) { - _showTapText(); - } - } - })); - - Observable debouncedEmitter = tapEventEmitter.debounce(1, TimeUnit.SECONDS); - Observable> debouncedBufferEmitter = tapEventEmitter.buffer(debouncedEmitter); - - _subscriptions// - .add(debouncedBufferEmitter// - .observeOn(AndroidSchedulers.mainThread())// - .subscribe(new Action1>() { - @Override - public void call(List taps) { - _showTapCount(taps.size()); - } - })); - } - - @Override - public void onStop() { - super.onStop(); - _subscriptions.unsubscribe(); - } - - // ----------------------------------------------------------------------------------- - // Helper to show the text via an animation - - private void _showTapText() { - _tapEventTxtShow.setVisibility(View.VISIBLE); - _tapEventTxtShow.setAlpha(1f); - ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400); - } - - private void _showTapCount(int size) { - _tapEventCountShow.setText(String.valueOf(size)); - _tapEventCountShow.setVisibility(View.VISIBLE); - _tapEventCountShow.setScaleX(1f); - _tapEventCountShow.setScaleY(1f); - ViewCompat.animate(_tapEventCountShow) - .scaleXBy(-1f) - .scaleYBy(-1f) - .setDuration(800) - .setStartDelay(100); - } +public class RxBusDemo_Bottom2Fragment extends BaseFragment { + + @BindView(R.id.demo_rxbus_tap_txt) + TextView _tapEventTxtShow; + + @BindView(R.id.demo_rxbus_tap_count) + TextView _tapEventCountShow; + + private RxBus _rxBus; + private CompositeDisposable _disposables; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false); + ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); + } + + @Override + public void onStart() { + super.onStart(); + _disposables = new CompositeDisposable(); + + Flowable tapEventEmitter = _rxBus.asFlowable().share(); + + _disposables.add( + tapEventEmitter.subscribe( + event -> { + if (event instanceof RxBusDemoFragment.TapEvent) { + _showTapText(); + } + })); + + Flowable debouncedEmitter = tapEventEmitter.debounce(1, TimeUnit.SECONDS); + Flowable> debouncedBufferEmitter = tapEventEmitter.buffer(debouncedEmitter); + + _disposables.add( + debouncedBufferEmitter + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + taps -> { + _showTapCount(taps.size()); + })); + } + + @Override + public void onStop() { + super.onStop(); + _disposables.clear(); + } + + // ----------------------------------------------------------------------------------- + // Helper to show the text via an animation + + private void _showTapText() { + _tapEventTxtShow.setVisibility(View.VISIBLE); + _tapEventTxtShow.setAlpha(1f); + ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400); + } + + private void _showTapCount(int size) { + _tapEventCountShow.setText(String.valueOf(size)); + _tapEventCountShow.setVisibility(View.VISIBLE); + _tapEventCountShow.setScaleX(1f); + _tapEventCountShow.setScaleY(1f); + ViewCompat.animate(_tapEventCountShow) + .scaleXBy(-1f) + .scaleYBy(-1f) + .setDuration(800) + .setStartDelay(100); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java index b956d58b..d45ec46e 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java @@ -7,104 +7,93 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.morihacky.android.rxjava.MainActivity; import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.fragments.BaseFragment; - -import java.util.List; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.flowables.ConnectableFlowable; import java.util.concurrent.TimeUnit; -import butterknife.Bind; -import butterknife.ButterKnife; -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.observables.ConnectableObservable; -import rx.subscriptions.CompositeSubscription; - -public class RxBusDemo_Bottom3Fragment - extends BaseFragment { - - @Bind(R.id.demo_rxbus_tap_txt) TextView _tapEventTxtShow; - @Bind(R.id.demo_rxbus_tap_count) TextView _tapEventCountShow; - private RxBus _rxBus; - private CompositeSubscription _subscriptions; - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); - } - - @Override - public void onStart() { - super.onStart(); - _subscriptions = new CompositeSubscription(); - - ConnectableObservable tapEventEmitter = _rxBus.toObserverable().publish(); - - _subscriptions// - .add(tapEventEmitter.subscribe(new Action1() { - @Override - public void call(Object event) { - if (event instanceof RxBusDemoFragment.TapEvent) { - _showTapText(); - } - } - })); - - _subscriptions// - .add(tapEventEmitter.publish(new Func1, Observable>>() { - @Override - public Observable> call(Observable stream) { - return stream.buffer(stream.debounce(1, TimeUnit.SECONDS)); - } - }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1>() { - @Override - public void call(List taps) { - _showTapCount(taps.size()); - } - })); - - _subscriptions.add(tapEventEmitter.connect()); - - } - - @Override - public void onStop() { - super.onStop(); - _subscriptions.clear(); - } - - // ----------------------------------------------------------------------------------- - // Helper to show the text via an animation - - private void _showTapText() { - _tapEventTxtShow.setVisibility(View.VISIBLE); - _tapEventTxtShow.setAlpha(1f); - ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400); - } - - private void _showTapCount(int size) { - _tapEventCountShow.setText(String.valueOf(size)); - _tapEventCountShow.setVisibility(View.VISIBLE); - _tapEventCountShow.setScaleX(1f); - _tapEventCountShow.setScaleY(1f); - ViewCompat.animate(_tapEventCountShow) - .scaleXBy(-1f) - .scaleYBy(-1f) - .setDuration(800) - .setStartDelay(100); - } +public class RxBusDemo_Bottom3Fragment extends BaseFragment { + + @BindView(R.id.demo_rxbus_tap_txt) + TextView _tapEventTxtShow; + + @BindView(R.id.demo_rxbus_tap_count) + TextView _tapEventCountShow; + + private RxBus _rxBus; + private CompositeDisposable _disposables; + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false); + ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); + } + + @Override + public void onStart() { + super.onStart(); + _disposables = new CompositeDisposable(); + + ConnectableFlowable tapEventEmitter = _rxBus.asFlowable().publish(); + + _disposables // + .add( + tapEventEmitter.subscribe( + event -> { + if (event instanceof RxBusDemoFragment.TapEvent) { + _showTapText(); + } + })); + + _disposables.add( + tapEventEmitter + .publish(stream -> stream.buffer(stream.debounce(1, TimeUnit.SECONDS))) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + taps -> { + _showTapCount(taps.size()); + })); + + _disposables.add(tapEventEmitter.connect()); + } + + @Override + public void onStop() { + super.onStop(); + _disposables.clear(); + } + + // ----------------------------------------------------------------------------------- + // Helper to show the text via an animation + + private void _showTapText() { + _tapEventTxtShow.setVisibility(View.VISIBLE); + _tapEventTxtShow.setAlpha(1f); + ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400); + } + + private void _showTapCount(int size) { + _tapEventCountShow.setText(String.valueOf(size)); + _tapEventCountShow.setVisibility(View.VISIBLE); + _tapEventCountShow.setScaleX(1f); + _tapEventCountShow.setScaleY(1f); + ViewCompat.animate(_tapEventCountShow) + .scaleXBy(-1f) + .scaleYBy(-1f) + .setDuration(800) + .setStartDelay(100); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java index bab619fd..7d6c58fb 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java @@ -11,30 +11,28 @@ import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.fragments.BaseFragment; -public class RxBusDemo_TopFragment - extends BaseFragment { +public class RxBusDemo_TopFragment extends BaseFragment { - private RxBus _rxBus; + private RxBus _rxBus; - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_rxbus_top, container, false); - ButterKnife.bind(this, layout); - return layout; - } + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_rxbus_top, container, false); + ButterKnife.bind(this, layout); + return layout; + } - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); - } + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _rxBus = ((MainActivity) getActivity()).getRxBusSingleton(); + } - @OnClick(R.id.btn_demo_rxbus_tap) - public void onTapButtonClicked() { - if (_rxBus.hasObservers()) { - _rxBus.send(new RxBusDemoFragment.TapEvent()); - } + @OnClick(R.id.btn_demo_rxbus_tap) + public void onTapButtonClicked() { + if (_rxBus.hasObservers()) { + _rxBus.send(new RxBusDemoFragment.TapEvent()); } + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/volley/MyVolley.java b/app/src/main/java/com/morihacky/android/rxjava/volley/MyVolley.java index d67b9433..a6ec55cf 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/volley/MyVolley.java +++ b/app/src/main/java/com/morihacky/android/rxjava/volley/MyVolley.java @@ -6,25 +6,23 @@ /** * Helper class that is used to provide references to initialized RequestQueue(s) and ImageLoader(s) - * - * @author Ognyan Bankov */ public class MyVolley { - private static RequestQueue mRequestQueue; + private static RequestQueue mRequestQueue; - private MyVolley() { - // no instances - } + private MyVolley() { + // no instances + } - public static void init(Context context) { - mRequestQueue = Volley.newRequestQueue(context); - } + public static void init(Context context) { + mRequestQueue = Volley.newRequestQueue(context); + } - public static RequestQueue getRequestQueue() { - if (mRequestQueue != null) { - return mRequestQueue; - } else { - throw new IllegalStateException("RequestQueue not initialized"); - } + static RequestQueue getRequestQueue() { + if (mRequestQueue != null) { + return mRequestQueue; + } else { + throw new IllegalStateException("RequestQueue not initialized"); } + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/volley/VolleyDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/volley/VolleyDemoFragment.java index 6d67d8d8..aaa55d08 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/volley/VolleyDemoFragment.java +++ b/app/src/main/java/com/morihacky/android/rxjava/volley/VolleyDemoFragment.java @@ -9,7 +9,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import com.android.volley.Request; @@ -19,141 +19,162 @@ import com.morihacky.android.rxjava.R; import com.morihacky.android.rxjava.fragments.BaseFragment; import com.morihacky.android.rxjava.wiring.LogAdapter; + +import butterknife.Unbinder; +import io.reactivex.Flowable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subscribers.DisposableSubscriber; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import org.json.JSONObject; -import rx.Observable; -import rx.Observer; -import rx.android.schedulers.AndroidSchedulers; -import rx.functions.Func0; -import rx.schedulers.Schedulers; -import rx.subscriptions.CompositeSubscription; import timber.log.Timber; -public class VolleyDemoFragment - extends BaseFragment { - - public static final String TAG = "VolleyDemoFragment"; - - @Bind(R.id.list_threading_log) ListView _logsList; - - private List _logs; - private LogAdapter _adapter; - - private CompositeSubscription _compositeSubscription = new CompositeSubscription(); - - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View layout = inflater.inflate(R.layout.fragment_volley, container, false); - ButterKnife.bind(this, layout); - return layout; - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - _setupLogger(); - } - - @Override - public void onPause() { - super.onPause(); - _compositeSubscription.unsubscribe(); - } - - public Observable newGetRouteData() { - return Observable.defer(new Func0>() { - @Override - public Observable call() { - try { - return Observable.just(getRouteData()); - } catch (InterruptedException | ExecutionException e) { - Log.e("routes", e.getMessage()); - return Observable.error(e); - } - } +public class VolleyDemoFragment extends BaseFragment { + + public static final String TAG = "VolleyDemoFragment"; + + @BindView(R.id.list_threading_log) + ListView _logsList; + + private List _logs; + private LogAdapter _adapter; + private Unbinder unbinder; + + private CompositeDisposable _disposables = new CompositeDisposable(); + + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View layout = inflater.inflate(R.layout.fragment_volley, container, false); + unbinder = ButterKnife.bind(this, layout); + return layout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + _setupLogger(); + } + + @Override + public void onPause() { + super.onPause(); + _disposables.clear(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + + /** + * Creates and returns an observable generated from the Future returned from {@code + * getRouteData()}. The observable can then be subscribed to as shown in {@code + * startVolleyRequest()} + * + * @return Observable + */ + public Flowable newGetRouteData() { + return Flowable.defer( + () -> { + try { + return Flowable.just(getRouteData()); + } catch (InterruptedException | ExecutionException e) { + Log.e("routes", e.getMessage()); + return Flowable.error(e); + } }); + } + + @OnClick(R.id.btn_start_operation) + void startRequest() { + startVolleyRequest(); + } + + private void startVolleyRequest() { + DisposableSubscriber d = + new DisposableSubscriber() { + @Override + public void onNext(JSONObject jsonObject) { + Log.e(TAG, "onNext " + jsonObject.toString()); + _log("onNext " + jsonObject.toString()); + } + + @Override + public void onError(Throwable e) { + VolleyError cause = (VolleyError) e.getCause(); + String s = new String(cause.networkResponse.data, Charset.forName("UTF-8")); + Log.e(TAG, s); + Log.e(TAG, cause.toString()); + _log("onError " + s); + } + + @Override + public void onComplete() { + Log.e(TAG, "onCompleted"); + Timber.d("----- onCompleted"); + _log("onCompleted "); + } + }; + + newGetRouteData() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(d); + + _disposables.add(d); + } + + /** + * Converts the Asynchronous Request into a Synchronous Future that can be used to block via + * {@code Future.get()}. Observables require blocking/synchronous functions + * + * @return JSONObject + * @throws ExecutionException + * @throws InterruptedException + */ + private JSONObject getRouteData() throws ExecutionException, InterruptedException { + RequestFuture future = RequestFuture.newFuture(); + String url = "http://www.weather.com.cn/adat/sk/101010100.html"; + JsonObjectRequest req = new JsonObjectRequest(Request.Method.GET, url, future, future); + MyVolley.getRequestQueue().add(req); + return future.get(); + } + + // ----------------------------------------------------------------------------------- + // Methods that help wiring up the example (irrelevant to RxJava) + + private void _setupLogger() { + _logs = new ArrayList<>(); + _adapter = new LogAdapter(getActivity(), new ArrayList<>()); + _logsList.setAdapter(_adapter); + } + + private void _log(String logMsg) { + + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) "); + _adapter.clear(); + _adapter.addAll(_logs); + } else { + _logs.add(0, logMsg + " (NOT main thread) "); + + // You can only do below stuff on main thread. + new Handler(Looper.getMainLooper()) + .post( + () -> { + _adapter.clear(); + _adapter.addAll(_logs); + }); } + } - @OnClick(R.id.btn_start_operation) - void startRequest() { - startVolleyRequest(); - } - - private void startVolleyRequest() { - _compositeSubscription.add(newGetRouteData().subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onCompleted() { - Log.e(TAG, "onCompleted"); - Timber.d("----- onCompleted"); - _log("onCompleted "); - } - - @Override - public void onError(Throwable e) { - VolleyError cause = (VolleyError) e.getCause(); - String s = new String(cause.networkResponse.data, Charset.forName("UTF-8")); - Log.e(TAG, s); - Log.e(TAG, cause.toString()); - _log("onError " + s); - - } - - @Override - public void onNext(JSONObject jsonObject) { - Log.e(TAG, "onNext " + jsonObject.toString()); - _log("onNext " + jsonObject.toString()); - - } - })); - } - - private JSONObject getRouteData() throws ExecutionException, InterruptedException { - RequestFuture future = RequestFuture.newFuture(); - String url = "http://www.weather.com.cn/adat/sk/101010100.html"; - final Request.Priority priority = Request.Priority.IMMEDIATE; - JsonObjectRequest req = new JsonObjectRequest(Request.Method.GET, url, future, future); - MyVolley.getRequestQueue().add(req); - return future.get(); - } - - // ----------------------------------------------------------------------------------- - // Methods that help wiring up the example (irrelevant to RxJava) - - private void _setupLogger() { - _logs = new ArrayList<>(); - _adapter = new LogAdapter(getActivity(), new ArrayList()); - _logsList.setAdapter(_adapter); - } - - private void _log(String logMsg) { - - if (_isCurrentlyOnMainThread()) { - _logs.add(0, logMsg + " (main thread) "); - _adapter.clear(); - _adapter.addAll(_logs); - } else { - _logs.add(0, logMsg + " (NOT main thread) "); - - // You can only do below stuff on main thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - - @Override - public void run() { - _adapter.clear(); - _adapter.addAll(_logs); - } - }); - } - } - - private boolean _isCurrentlyOnMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } + private boolean _isCurrentlyOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } } diff --git a/app/src/main/java/com/morihacky/android/rxjava/wiring/LogAdapter.java b/app/src/main/java/com/morihacky/android/rxjava/wiring/LogAdapter.java index 0c0ea1e9..cfa108f1 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/wiring/LogAdapter.java +++ b/app/src/main/java/com/morihacky/android/rxjava/wiring/LogAdapter.java @@ -5,10 +5,9 @@ import com.morihacky.android.rxjava.R; import java.util.List; -public class LogAdapter - extends ArrayAdapter { +public class LogAdapter extends ArrayAdapter { - public LogAdapter(Context context, List logs) { - super(context, R.layout.item_log, R.id.item_log, logs); - } + public LogAdapter(Context context, List logs) { + super(context, R.layout.item_log, R.id.item_log, logs); + } } diff --git a/app/src/main/kotlin/com/morihacky/android/rxjava/ext/RxExt.kt b/app/src/main/kotlin/com/morihacky/android/rxjava/ext/RxExt.kt new file mode 100644 index 00000000..9c99ed69 --- /dev/null +++ b/app/src/main/kotlin/com/morihacky/android/rxjava/ext/RxExt.kt @@ -0,0 +1,11 @@ +package com.morihacky.android.rxjava.ext + +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + +operator fun CompositeDisposable.plus(disposable: Disposable): CompositeDisposable { + add(disposable) + return this +} + + diff --git a/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/MulticastPlaygroundFragment.kt b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/MulticastPlaygroundFragment.kt new file mode 100644 index 00000000..5ece0e32 --- /dev/null +++ b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/MulticastPlaygroundFragment.kt @@ -0,0 +1,166 @@ +package com.morihacky.android.rxjava.fragments + +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import com.jakewharton.rx.replayingShare +import com.morihacky.android.rxjava.R +import io.reactivex.Observable +import io.reactivex.disposables.Disposable +import java.util.concurrent.TimeUnit + +class MulticastPlaygroundFragment : BaseFragment() { + + @BindView(R.id.list_threading_log) lateinit var logList: ListView + @BindView(R.id.dropdown) lateinit var pickOperatorDD: Spinner + @BindView(R.id.msg_text) lateinit var messageText: TextView + + private lateinit var sharedObservable: Observable + private lateinit var adapter: LogAdapter + + private var logs: MutableList = ArrayList() + private var disposable1: Disposable? = null + private var disposable2: Disposable? = null + + override fun onCreateView(inflater: LayoutInflater?, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val layout = inflater!!.inflate(R.layout.fragment_multicast_playground, container, false) + ButterKnife.bind(this, layout) + + _setupLogger() + _setupDropdown() + + return layout + } + + @OnClick(R.id.btn_1) + fun onBtn1Click() { + + disposable1?.let { + it.dispose() + _log("subscriber 1 disposed") + disposable1 = null + return + } + + disposable1 = + sharedObservable + .doOnSubscribe { _log("subscriber 1 (subscribed)") } + .subscribe({ long -> _log("subscriber 1: onNext $long") }) + + } + + @OnClick(R.id.btn_2) + fun onBtn2Click() { + disposable2?.let { + it.dispose() + _log("subscriber 2 disposed") + disposable2 = null + return + } + + disposable2 = + sharedObservable + .doOnSubscribe { _log("subscriber 2 (subscribed)") } + .subscribe({ long -> _log("subscriber 2: onNext $long") }) + } + + @OnClick(R.id.btn_3) + fun onBtn3Click() { + logs = ArrayList() + adapter.clear() + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private fun _log(logMsg: String) { + + if (_isCurrentlyOnMainThread()) { + logs.add(0, logMsg + " (main thread) ") + adapter.clear() + adapter.addAll(logs) + } else { + logs.add(0, logMsg + " (NOT main thread) ") + + // You can only do below stuff on main thread. + Handler(Looper.getMainLooper()).post { + adapter.clear() + adapter.addAll(logs) + } + } + } + + private fun _setupLogger() { + logs = ArrayList() + adapter = LogAdapter(activity, ArrayList()) + logList.adapter = adapter + } + + private fun _setupDropdown() { + pickOperatorDD.adapter = ArrayAdapter(context, + android.R.layout.simple_spinner_dropdown_item, + arrayOf(".publish().refCount()", + ".publish().autoConnect(2)", + ".replay(1).autoConnect(2)", + ".replay(1).refCount()", + ".replayingShare()")) + + + pickOperatorDD.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, index: Int, p3: Long) { + + val sourceObservable = Observable.interval(0L, 3, TimeUnit.SECONDS) + .doOnSubscribe { _log("observer (subscribed)") } + .doOnDispose { _log("observer (disposed)") } + .doOnTerminate { _log("observer (terminated)") } + + sharedObservable = + when (index) { + 0 -> { + messageText.setText(R.string.msg_demo_multicast_publishRefCount) + sourceObservable.publish().refCount() + } + 1 -> { + messageText.setText(R.string.msg_demo_multicast_publishAutoConnect) + sourceObservable.publish().autoConnect(2) + } + 2 -> { + messageText.setText(R.string.msg_demo_multicast_replayAutoConnect) + sourceObservable.replay(1).autoConnect(2) + } + 3 -> { + messageText.setText(R.string.msg_demo_multicast_replayRefCount) + sourceObservable.replay(1).refCount() + } + 4 -> { + messageText.setText(R.string.msg_demo_multicast_replayingShare) + sourceObservable.replayingShare() + } + else -> throw RuntimeException("got to pick an op yo!") + } + } + + override fun onNothingSelected(p0: AdapterView<*>?) {} + } + } + + private fun _isCurrentlyOnMainThread(): Boolean { + return Looper.myLooper() == Looper.getMainLooper() + } + + private inner class LogAdapter(context: Context, logs: List) : + ArrayAdapter(context, R.layout.item_log, R.id.item_log, logs) + +} + diff --git a/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt new file mode 100644 index 00000000..cf77e04f --- /dev/null +++ b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt @@ -0,0 +1,67 @@ +package com.morihacky.android.rxjava.fragments + +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ListView +import com.morihacky.android.rxjava.R + +class PlaygroundFragment : BaseFragment() { + + private var _logsList: ListView? = null + private var _adapter: LogAdapter? = null + + private var _logs: MutableList = ArrayList() + + override fun onCreateView(inflater: LayoutInflater?, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater?.inflate(R.layout.fragment_concurrency_schedulers, container, false) + + _logsList = view?.findViewById(R.id.list_threading_log) as ListView + _setupLogger() + + view.findViewById(R.id.btn_start_operation).setOnClickListener { _ -> + _log("Button clicked") + } + + return view + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private fun _log(logMsg: String) { + + if (_isCurrentlyOnMainThread()) { + _logs.add(0, logMsg + " (main thread) ") + _adapter?.clear() + _adapter?.addAll(_logs) + } else { + _logs.add(0, logMsg + " (NOT main thread) ") + + // You can only do below stuff on main thread. + Handler(Looper.getMainLooper()).post { + _adapter?.clear() + _adapter?.addAll(_logs) + } + } + } + + private fun _setupLogger() { + _logs = ArrayList() + _adapter = LogAdapter(activity, ArrayList()) + _logsList?.adapter = _adapter + } + + private fun _isCurrentlyOnMainThread(): Boolean { + return Looper.myLooper() == Looper.getMainLooper() + } + + private inner class LogAdapter(context: Context, logs: List) : ArrayAdapter(context, R.layout.item_log, R.id.item_log, logs) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/UsingFragment.kt b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/UsingFragment.kt new file mode 100644 index 00000000..d268609d --- /dev/null +++ b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/UsingFragment.kt @@ -0,0 +1,93 @@ +package com.morihacky.android.rxjava.fragments + +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +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 com.morihacky.android.rxjava.R +import io.reactivex.Flowable +import io.reactivex.functions.Consumer +import io.reactivex.functions.Function +import org.reactivestreams.Publisher +import java.util.* +import java.util.concurrent.Callable + +class UsingFragment : BaseFragment() { + + private lateinit var _logs: MutableList + private lateinit var _logsList: ListView + private lateinit var _adapter: UsingFragment.LogAdapter + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater?.inflate(R.layout.fragment_buffer, container, false) + _logsList = view?.findViewById(R.id.list_threading_log) as ListView + + (view.findViewById(R.id.text_description) as TextView).setText(R.string.msg_demo_using) + + _setupLogger() + view.findViewById(R.id.btn_start_operation).setOnClickListener { executeUsingOperation() } + return view + } + + private fun executeUsingOperation() { + val resourceSupplier = Callable { Realm() } + val sourceSupplier = Function> { realm -> + Flowable.just(true) + .map { + realm.doSomething() + // i would use the copyFromRealm and change it to a POJO + Random().nextInt(50) + } + } + val disposer = Consumer { realm -> + realm.clear() + } + + Flowable.using(resourceSupplier, sourceSupplier, disposer) + .subscribe({ i -> + _log("got a value $i - (look at the logs)") + }) + } + + inner class Realm { + init { + _log("initializing Realm instance") + } + + fun doSomething() { + _log("do something with Realm instance") + } + + fun clear() { + // notice how this is called even before you manually "dispose" + _log("cleaning up the resources (happens before a manual 'dispose'") + } + } + + // ----------------------------------------------------------------------------------- + // Method that help wiring up the example (irrelevant to RxJava) + + private fun _log(logMsg: String) { + _logs.add(0, logMsg) + + // You can only do below stuff on main thread. + Handler(Looper.getMainLooper()).post { + _adapter.clear() + _adapter.addAll(_logs) + } + } + + private fun _setupLogger() { + _logs = ArrayList() + _adapter = LogAdapter(activity, ArrayList()) + _logsList.adapter = _adapter + } + + private class LogAdapter(context: Context, logs: List) : ArrayAdapter(context, R.layout.item_log, R.id.item_log, logs) +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_buffer.xml b/app/src/main/res/layout/fragment_buffer.xml index f2bbaadf..3464ac5b 100644 --- a/app/src/main/res/layout/fragment_buffer.xml +++ b/app/src/main/res/layout/fragment_buffer.xml @@ -1,5 +1,4 @@ - - - diff --git a/app/src/main/res/layout/fragment_demo_timing.xml b/app/src/main/res/layout/fragment_demo_timing.xml index 4129d5a7..0cb4286e 100644 --- a/app/src/main/res/layout/fragment_demo_timing.xml +++ b/app/src/main/res/layout/fragment_demo_timing.xml @@ -1,5 +1,4 @@ - @@ -39,7 +37,7 @@ android:layout_weight="1" android:layout_height="wrap_content" android:layout_width="wrap_content" - android:text="BTN 1" + android:text="@string/btn_1" />