From ea5877bd6d58409ba6a79ccc5fd85b139b70dca3 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Wed, 24 May 2017 15:17:31 -0700 Subject: [PATCH 01/11] --wip-- --- .../android/rxjava/fragments/UsingFragment.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt b/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt new file mode 100644 index 00000000..1767cf80 --- /dev/null +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt @@ -0,0 +1,17 @@ +package com.morihacky.android.rxjava.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +class UsingFragment : BaseFragment() { + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + } +} \ No newline at end of file From bc5bcc392a45c8ca98332b021f365fdde4ff7d8b Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Wed, 24 May 2017 20:45:22 -0700 Subject: [PATCH 02/11] fix: cleanup from auto java -> kotlin conversion --- .../rxjava/fragments/PlaygroundFragment.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt b/app/src/main/java/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt index 37f76532..cf77e04f 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt @@ -16,25 +16,21 @@ class PlaygroundFragment : BaseFragment() { private var _logsList: ListView? = null private var _adapter: LogAdapter? = null - private var _attempt = 0 private var _logs: MutableList<String> = ArrayList() override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater!!.inflate(R.layout.fragment_concurrency_schedulers, container, false) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + val view = inflater?.inflate(R.layout.fragment_concurrency_schedulers, container, false) - _logsList = activity.findViewById(R.id.list_threading_log) as ListView + _logsList = view?.findViewById(R.id.list_threading_log) as ListView + _setupLogger() - activity.findViewById(R.id.btn_start_operation).setOnClickListener { _ -> + view.findViewById(R.id.btn_start_operation).setOnClickListener { _ -> _log("Button clicked") } - _setupLogger() + return view } // ----------------------------------------------------------------------------------- @@ -44,15 +40,15 @@ class PlaygroundFragment : BaseFragment() { if (_isCurrentlyOnMainThread()) { _logs.add(0, logMsg + " (main thread) ") - _adapter!!.clear() - _adapter!!.addAll(_logs) + _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) + _adapter?.clear() + _adapter?.addAll(_logs) } } } @@ -60,7 +56,7 @@ class PlaygroundFragment : BaseFragment() { private fun _setupLogger() { _logs = ArrayList<String>() _adapter = LogAdapter(activity, ArrayList<String>()) - _logsList!!.adapter = _adapter + _logsList?.adapter = _adapter } private fun _isCurrentlyOnMainThread(): Boolean { From c36b9ade6b4e0bc8429702d6afa32c7ab83c2866 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Wed, 24 May 2017 21:19:13 -0700 Subject: [PATCH 03/11] feat: add using operator example [kotlin] --- README.md | 7 ++ .../android/rxjava/fragments/UsingFragment.kt | 86 ++++++++++++++++++- app/src/main/res/layout/fragment_buffer.xml | 3 +- app/src/main/res/values/strings.xml | 1 + 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fa0e39b0..987733c9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ I've also been giving talks about Learning Rx using many of the examples listed 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) ## Description @@ -222,6 +223,12 @@ This is a simple example demonstrating the use of the `.timeout` operator. Butto 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. + ## Rx 2.x All the examples here have been migrated to use RxJava 2.X. diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt b/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt index 1767cf80..b3108aa3 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt @@ -1,17 +1,97 @@ 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.disposables.Disposable +import io.reactivex.functions.Consumer +import io.reactivex.functions.Function +import org.reactivestreams.Publisher +import timber.log.Timber +import java.util.* +import java.util.concurrent.Callable class UsingFragment : BaseFragment() { + private var _logs: MutableList<String> = ArrayList() + private var _logsList: ListView? = null + private var _adapter: UsingFragment.LogAdapter? = null + private var disposable: Disposable? = null + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return super.onCreateView(inflater, container, savedInstanceState) + 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> { Realm() } + val sourceSupplier = Function<Realm, Publisher<Int>> { 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 -> + realm.clear() + } + + disposable = + Flowable.using(resourceSupplier, sourceSupplier, disposer) + .subscribe({ i -> + _log("got a value $i - (look at the logs)") + }) } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + inner class Realm { + init { + Timber.d("--- initializing Realm instance") + } + + fun doSomething() { + Timber.d("--- do something with Realm instance") + } + + fun clear() { + // notice how this is called even before you manually "dispose" + Timber.d("--- 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<String>() + _adapter = LogAdapter(activity, ArrayList<String>()) + _logsList?.adapter = _adapter + } + + private class LogAdapter(context: Context, logs: List<String>) : ArrayAdapter<String>(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 86d70150..3464ac5b 100644 --- a/app/src/main/res/layout/fragment_buffer.xml +++ b/app/src/main/res/layout/fragment_buffer.xml @@ -7,7 +7,8 @@ > <TextView - android:layout_height="wrap_content" + android:id="@+id/text_description" + android:layout_height="wrap_content" android:layout_width="match_parent" android:padding="10dp" android:gravity="center" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3831a0c..aa826bc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ <string name="msg_demo_timing">BTN 1: run single task once (after 2s complete)\nBTN 2: run task every 1s (start delay of 1s) toggle \nBTN 3: run task every 1s (start immediately) toggle \nBTN 4: run task 5 times every 3s (then complete) \nBTN 5: run task A, pause for sometime, then proceed with Task B</string> <string name="msg_demo_rotation_persist">This is an example of starting an Observable and using the result across rotations. There are many ways to do this, we use a retained fragment in this example</string> <string name="msg_demo_network_detector">This is a demo of how to use Subjects to detect Network connectivity\nToggle your Wifi/Network on or off and notice the logs</string> + <string name="msg_demo_using">This is a demo of the somewhat unknown operator "using".\n\nmsg_demo_usingYou typically use it for managing setup/teardown of resources. Classic cases are DB connections (like Realm), sockets, locks etc.\n\nTap the button and look at the logcat. Particularly notice how the Realm instance is self-contained. That is, it is auto-disposed right after use.</string> <string name="msg_pseudoCache_demoInfo_concat">Concat merges the results sequentially. But notice that the latter subscription starts only AFTER the first one completes. Some unnecessary waiting there.</string> <string name="msg_pseudoCache_demoInfo_concatEager">Concat eager is cooler. Both subscriptions start at the same time (parallely) but the order of emission is respected.</string> From f42c9a82112da63620b74ead30f0e9beb43196e2 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Wed, 24 May 2017 21:21:49 -0700 Subject: [PATCH 04/11] feat: link it to a button --- .../morihacky/android/rxjava/fragments/MainFragment.java | 5 +++++ app/src/main/res/layout/fragment_main.xml | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+) 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 c09925be..3874accf 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 @@ -116,6 +116,11 @@ void demoNetworkDetector() { clickedOn(new NetworkDetectorFragment()); } + @OnClick(R.id.btn_demo_using) + void demoUsing() { + clickedOn(new UsingFragment()); + } + private void clickedOn(@NonNull Fragment fragment) { final String tag = fragment.getClass().toString(); getActivity() diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 51f0a694..adf5ff8f 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -127,5 +127,12 @@ android:layout_width="match_parent" android:text="@string/btn_demo_networkDetector" /> + + <Button + android:id="@+id/btn_demo_using" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:text="@string/btn_demo_using" + /> </LinearLayout> </ScrollView> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa826bc7..510cfcac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ <string name="btn_demo_pagination">Paging example</string> <string name="btn_demo_pagination_more">MOAR</string> <string name="btn_demo_networkDetector">Network Detector (Subject)</string> + <string name="btn_demo_using">Setup & teardown resources (using)</string> <string name="msg_demo_pagination">This is a demo of how you can do a list pagination with Rx. We page 10 items at a time and there are 55 items altogether</string> <string name="msg_demo_volley">This is a Volley request demo</string> From 546bd2d0066bb855659e0cfaef3c07a3bb5a8b7f Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Fri, 26 May 2017 14:21:35 -0700 Subject: [PATCH 05/11] ref: move to kotlin folder + RxExt use RxExtensions courtesy: https://github.com/dlew/android-architecture-counter-sample --- .../kotlin/com/morihacky/android/rxjava/ext/RxExt.kt | 11 +++++++++++ .../android/rxjava/fragments/PlaygroundFragment.kt | 0 .../android/rxjava/fragments/UsingFragment.kt | 0 3 files changed, 11 insertions(+) create mode 100644 app/src/main/kotlin/com/morihacky/android/rxjava/ext/RxExt.kt rename app/src/main/{java => kotlin}/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt (100%) rename app/src/main/{java => kotlin}/com/morihacky/android/rxjava/fragments/UsingFragment.kt (100%) 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/java/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt similarity index 100% rename from app/src/main/java/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt rename to app/src/main/kotlin/com/morihacky/android/rxjava/fragments/PlaygroundFragment.kt diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt b/app/src/main/kotlin/com/morihacky/android/rxjava/fragments/UsingFragment.kt similarity index 100% rename from app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt rename to app/src/main/kotlin/com/morihacky/android/rxjava/fragments/UsingFragment.kt From 8bb75ebacab17c0f76839724aad4b358bc711b8c Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Sat, 1 Jul 2017 18:26:53 -0700 Subject: [PATCH 06/11] fix: clean up kotlin code --- .../android/rxjava/fragments/UsingFragment.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt b/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt index b3108aa3..1b9ee372 100644 --- a/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt +++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/UsingFragment.kt @@ -12,7 +12,6 @@ import android.widget.ListView import android.widget.TextView import com.morihacky.android.rxjava.R import io.reactivex.Flowable -import io.reactivex.disposables.Disposable import io.reactivex.functions.Consumer import io.reactivex.functions.Function import org.reactivestreams.Publisher @@ -22,10 +21,9 @@ import java.util.concurrent.Callable class UsingFragment : BaseFragment() { - private var _logs: MutableList<String> = ArrayList() - private var _logsList: ListView? = null - private var _adapter: UsingFragment.LogAdapter? = null - private var disposable: Disposable? = null + private lateinit var _logs: MutableList<String> + 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) @@ -52,11 +50,10 @@ class UsingFragment : BaseFragment() { realm.clear() } - disposable = - Flowable.using(resourceSupplier, sourceSupplier, disposer) - .subscribe({ i -> - _log("got a value $i - (look at the logs)") - }) + Flowable.using(resourceSupplier, sourceSupplier, disposer) + .subscribe({ i -> + _log("got a value $i - (look at the logs)") + }) } inner class Realm { @@ -82,15 +79,15 @@ class UsingFragment : BaseFragment() { // You can only do below stuff on main thread. Handler(Looper.getMainLooper()).post { - _adapter!!.clear() - _adapter!!.addAll(_logs) + _adapter.clear() + _adapter.addAll(_logs) } } private fun _setupLogger() { _logs = ArrayList<String>() _adapter = LogAdapter(activity, ArrayList<String>()) - _logsList?.adapter = _adapter + _logsList.adapter = _adapter } private class LogAdapter(context: Context, logs: List<String>) : ArrayAdapter<String>(context, R.layout.item_log, R.id.item_log, logs) From 27a5758e180d8df9a77cc5c2d6d1c6a2062d1ee1 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Sun, 2 Jul 2017 11:57:23 -0700 Subject: [PATCH 07/11] chore: update agp + cradle --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4ec27655..ff8d245f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.2' + classpath 'com.android.tools.build:gradle:2.3.3' classpath 'com.f2prateek.javafmt:javafmt:0.1.2' // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 36a044e4..391ac683 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip From da119492b461f3c8e6029ef59388c8dad02937ec Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Sun, 2 Jul 2017 11:58:12 -0700 Subject: [PATCH 08/11] chore: make app multi-dex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cause I don’t want to deal with 65K method limit now --- app/build.gradle | 3 +++ app/src/main/java/com/morihacky/android/rxjava/MyApp.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 74a7d975..84f14d45 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,6 +18,7 @@ apply plugin: 'com.f2prateek.javafmt' apply plugin: 'kotlin-android' dependencies { + 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}" @@ -46,6 +47,7 @@ dependencies { 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' @@ -65,6 +67,7 @@ android { targetSdkVersion sdkVersion versionCode 2 versionName "1.2" + multiDexEnabled true } buildTypes { release { 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 a3b025ac..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,12 +1,12 @@ 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; From 67b860c2d0be71514b8af28364e5139ad7c8bb75 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Sun, 2 Jul 2017 11:59:00 -0700 Subject: [PATCH 09/11] feat: add multicast playground example --- app/build.gradle | 3 +- .../fragments/MulticastPlaygroundFragment.kt | 166 ++++++++++++++++++ .../layout/fragment_multicast_playground.xml | 72 ++++++++ app/src/main/res/values/strings.xml | 5 + 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/com/morihacky/android/rxjava/fragments/MulticastPlaygroundFragment.kt create mode 100644 app/src/main/res/layout/fragment_multicast_playground.xml diff --git a/app/build.gradle b/app/build.gradle index 84f14d45..022d666d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ dependencies { compile 'com.github.kaushikgopal:CoreTextUtils:c703fa12b6' compile "com.jakewharton:butterknife:${butterKnifeVersion}" - annotationProcessor "com.jakewharton:butterknife-compiler:${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}" @@ -45,6 +45,7 @@ dependencies { // explicitly depend on RxJava's latest version for bug fixes and new features. 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' 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<Long> + private lateinit var adapter: LogAdapter + + private var logs: MutableList<String> = 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<String>() + 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<String>() + adapter = LogAdapter(activity, ArrayList<String>()) + logList.adapter = adapter + } + + private fun _setupDropdown() { + pickOperatorDD.adapter = ArrayAdapter<String>(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<String>) : + ArrayAdapter<String>(context, R.layout.item_log, R.id.item_log, logs) + +} + diff --git a/app/src/main/res/layout/fragment_multicast_playground.xml b/app/src/main/res/layout/fragment_multicast_playground.xml new file mode 100644 index 00000000..9e5d025e --- /dev/null +++ b/app/src/main/res/layout/fragment_multicast_playground.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + + <TableLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TableRow + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"> + + <Spinner + android:id="@+id/dropdown" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_span="3"/> + + </TableRow> + + <TableRow + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/msg_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_span="3" + android:padding="@dimen/text_micro" + android:text="@string/msg_demo_multicast_publishRefCount"/> + </TableRow> + + <TableRow + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/btn_1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Subscribe 1"/> + + <Button + android:id="@+id/btn_2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Subscribe 2"/> + + <Button + android:id="@+id/btn_3" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Clear log"/> + </TableRow> + + </TableLayout> + + + <ListView + android:id="@+id/list_threading_log" + android:layout_width="match_parent" + android:layout_height="match_parent"/> +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 510cfcac..4bc2e026 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,11 @@ <string name="msg_demo_rotation_persist">This is an example of starting an Observable and using the result across rotations. There are many ways to do this, we use a retained fragment in this example</string> <string name="msg_demo_network_detector">This is a demo of how to use Subjects to detect Network connectivity\nToggle your Wifi/Network on or off and notice the logs</string> <string name="msg_demo_using">This is a demo of the somewhat unknown operator "using".\n\nmsg_demo_usingYou typically use it for managing setup/teardown of resources. Classic cases are DB connections (like Realm), sockets, locks etc.\n\nTap the button and look at the logcat. Particularly notice how the Realm instance is self-contained. That is, it is auto-disposed right after use.</string> + <string name="msg_demo_multicast_publishRefCount">RefCount starts the upstream right away and gets disposed off, when all subscribers stop. Hit S1, Hit S2, Hit S1, Hit S2. Hit S1/S2 now and notice the stream starts all over.</string> + <string name="msg_demo_multicast_publishAutoConnect">AutoConnect(2) waits for a min. subscriber count, before starting the upstream. Hit S1 (notice events don\'t start), Hit S2 (notice events now start), Hit S1 (notice that unsubscribing doesn\'t affect upstream), Hit S2, wait for sometime and hit S1 again (notice source stream doesn\'t restart)</string> + <string name="msg_demo_multicast_replayAutoConnect">Replay caches the last item. Hit S1, Hit S2, event starts, Hit S2, wait a bit, Hit S2 again (notice it starts with the last item that S1 saw - courtesy Replay). Hit S2, Hit S1, wait a bit. Hit S1 again (notice event upstream continues and doesn\'t restart)</string> + <string name="msg_demo_multicast_replayRefCount">Replay caches the last item. Hit S1, wait a bit, then hit S2 (notice S2 starts immediately with last item that S1 saw), Hit S2, Hit S1. Hit S1/S2 again (notice the stream restarts all over. Interestingly cached last item also removed when both subscribers released)</string> + <string name="msg_demo_multicast_replayingShare">Courtesy: new #AndroidDev on the block - JakeWharton. exactly like replay(1).refCount(), but caches the last item even when upstream has been disposed off/released. Hit S1, Hit S2, Hit S1, Hit S2 (notice observable is disposed). Hit S1/S2 again (notice we start with last item emitted)</string> <string name="msg_pseudoCache_demoInfo_concat">Concat merges the results sequentially. But notice that the latter subscription starts only AFTER the first one completes. Some unnecessary waiting there.</string> <string name="msg_pseudoCache_demoInfo_concatEager">Concat eager is cooler. Both subscriptions start at the same time (parallely) but the order of emission is respected.</string> From a53b975ac3f4889536b3a4edc4f43bf380e10b00 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Mon, 17 Jul 2017 10:14:20 -0700 Subject: [PATCH 10/11] feat: cleanup and add README notes --- README.md | 13 +++++++++++-- .../android/rxjava/fragments/MainFragment.java | 5 +++++ app/src/main/res/layout/fragment_main.xml | 7 +++++++ app/src/main/res/values/strings.xml | 1 + build.gradle | 2 +- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 987733c9..5b5cdbcf 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ I've also been giving talks about Learning Rx using many of the examples listed 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]() ## Description @@ -162,7 +163,7 @@ Cases demonstrated here: 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 -### 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) There are accompanying blog posts that do a much better job of explaining the details on this demo: @@ -227,7 +228,15 @@ Notice how we can provide a custom Observable that indicates how to react under 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. +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 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 3874accf..dd5709e1 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 @@ -121,6 +121,11 @@ 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() diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index adf5ff8f..6b29ee24 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -134,5 +134,12 @@ android:layout_width="match_parent" android:text="@string/btn_demo_using" /> + + <Button + android:id="@+id/btn_demo_multicastPlayground" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:text="@string/btn_demo_multicastPlayground" + /> </LinearLayout> </ScrollView> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4bc2e026..8242d0c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ <string name="btn_demo_pagination_more">MOAR</string> <string name="btn_demo_networkDetector">Network Detector (Subject)</string> <string name="btn_demo_using">Setup & teardown resources (using)</string> + <string name="btn_demo_multicastPlayground">MultiConnect operator playground</string> <string name="msg_demo_pagination">This is a demo of how you can do a list pagination with Rx. We page 10 items at a time and there are 55 items altogether</string> <string name="msg_demo_volley">This is a Volley request demo</string> diff --git a/build.gradle b/build.gradle index ff8d245f..9a955883 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ allprojects { sdkVersion = 24 buildToolsVrs = "25.0.0" - kotlinVersion = "1.1.2-4" + kotlinVersion = "1.1.3-2" butterKnifeVersion = '8.5.1' mockitoKotlinVersion = "1.4.0" From 25644fc01e4368e4fb9c265b0d34607535c6435a Mon Sep 17 00:00:00 2001 From: Kaushik Gopal <code@kaush.co> Date: Mon, 17 Jul 2017 10:16:41 -0700 Subject: [PATCH 11/11] fix: add README link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b5cdbcf..36c7903d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ I've also been giving talks about Learning Rx using many of the examples listed 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](#18-multicast-playground) ## Description