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/BaseFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/BaseFragment.java
new file mode 100644
index 00000000..4ce242f3
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/BaseFragment.java
@@ -0,0 +1,15 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.support.v4.app.Fragment;
+import com.morihacky.android.rxjava.MyApp;
+import com.squareup.leakcanary.RefWatcher;
+
+public class BaseFragment extends Fragment {
+
+ @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
new file mode 100644
index 00000000..bf51b85c
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/BufferDemoFragment.java
@@ -0,0 +1,160 @@
+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.Button;
+import android.widget.ListView;
+
+import com.jakewharton.rxbinding2.view.RxView;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.wiring.LogAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+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
+ * 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.
+ *
+ *
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 {
+
+ @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();
+ }
+}
diff --git a/app/src/main/java/com/morihacky/android/rxjava/ConcurrencyWithSchedulersDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java
similarity index 53%
rename from app/src/main/java/com/morihacky/android/rxjava/ConcurrencyWithSchedulersDemoFragment.java
rename to app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java
index 03611474..9a9a61ea 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/ConcurrencyWithSchedulersDemoFragment.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/ConcurrencyWithSchedulersDemoFragment.java
@@ -1,48 +1,49 @@
-package com.morihacky.android.rxjava;
+package com.morihacky.android.rxjava.fragments;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
+import butterknife.BindView;
import butterknife.ButterKnife;
-import butterknife.InjectView;
import butterknife.OnClick;
-import com.morihacky.android.rxjava.app.R;
+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 rx.Observable;
-import rx.Observer;
-import rx.Subscriber;
-import rx.Subscription;
-import rx.android.observables.AndroidObservable;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.schedulers.Schedulers;
import timber.log.Timber;
-public class ConcurrencyWithSchedulersDemoFragment
- extends Fragment {
+public class ConcurrencyWithSchedulersDemoFragment extends BaseFragment {
- @InjectView(R.id.progress_operation_running) ProgressBar _progress;
- @InjectView(R.id.list_threading_log) ListView _logsList;
+ @BindView(R.id.progress_operation_running)
+ ProgressBar _progress;
+
+ @BindView(R.id.list_threading_log)
+ ListView _logsList;
private LogAdapter _adapter;
private List _logs;
- private Subscription _subscription;
+ private CompositeDisposable _disposables = new CompositeDisposable();
+ private Unbinder unbinder;
@Override
public void onDestroy() {
super.onDestroy();
- if (_subscription != null) {
- _subscription.unsubscribe();
- }
+ unbinder.unbind();
+ _disposables.clear();
}
@Override
@@ -52,11 +53,10 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
}
@Override
- public View onCreateView(LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_concurrency_schedulers, container, false);
- ButterKnife.inject(this, layout);
+ unbinder = ButterKnife.bind(this, layout);
return layout;
}
@@ -66,40 +66,36 @@ public void startLongOperation() {
_progress.setVisibility(View.VISIBLE);
_log("Button Clicked");
- _subscription = AndroidObservable.bindFragment(this, _getObservable()) // Observable
+ DisposableObserver d = _getDisposableObserver();
+
+ _getObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(_getObserver()); // Observer
+ .subscribe(d);
+
+ _disposables.add(d);
}
private Observable _getObservable() {
- return Observable.create(new Observable.OnSubscribe() {
-
- @Override
- public void call(Subscriber super Boolean> observer) {
-
- _log("Within Observable");
-
- _doSomeLongOperation_thatBlocksCurrentThread();
- observer.onNext(true);
- observer.onCompleted();
- }
- });
+ return Observable.just(true)
+ .map(
+ aBoolean -> {
+ _log("Within Observable");
+ _doSomeLongOperation_thatBlocksCurrentThread();
+ return aBoolean;
+ });
}
/**
- * Observer that handles the result List from Observable
- * through the 3 important actions:
+ * Observer that handles the result through the 3 important actions:
*
- * 1. onCompleted
- * 2. onError
- * 3. onNext
+ * 1. onCompleted 2. onError 3. onNext
*/
- private Observer _getObserver() {
- return new Observer() {
+ private DisposableObserver _getDisposableObserver() {
+ return new DisposableObserver() {
@Override
- public void onCompleted() {
+ public void onComplete() {
_log("On complete");
_progress.setVisibility(View.INVISIBLE);
}
@@ -107,13 +103,13 @@ public void onCompleted() {
@Override
public void onError(Throwable e) {
Timber.e(e, "Error in RxJava Demo concurrency");
- _log(String.format("Boo Error %s", e.getMessage()));
+ _log(String.format("Boo! Error %s", e.getMessage()));
_progress.setVisibility(View.INVISIBLE);
}
@Override
- public void onNext(Boolean aBoolean) {
- _log(String.format("onNext with return value \"%b\"", aBoolean));
+ public void onNext(Boolean bool) {
+ _log(String.format("onNext with return value \"%b\"", bool));
}
};
}
@@ -141,20 +137,18 @@ private void _log(String logMsg) {
_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);
- }
- });
+ new Handler(Looper.getMainLooper())
+ .post(
+ () -> {
+ _adapter.clear();
+ _adapter.addAll(_logs);
+ });
}
}
private void _setupLogger() {
- _logs = new ArrayList();
- _adapter = new LogAdapter(getActivity(), new ArrayList());
+ _logs = new ArrayList<>();
+ _adapter = new LogAdapter(getActivity(), new ArrayList<>());
_logsList.setAdapter(_adapter);
}
@@ -162,11 +156,10 @@ 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);
}
}
-}
\ 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
new file mode 100644
index 00000000..2d62cff7
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/DebounceSearchEmitterFragment.java
@@ -0,0 +1,145 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.content.Context;
+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.EditText;
+import android.widget.ListView;
+
+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.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+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 {
+
+ @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 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/DoubleBindingTextViewFragment.java b/app/src/main/java/com/morihacky/android/rxjava/fragments/DoubleBindingTextViewFragment.java
new file mode 100644
index 00000000..883f043e
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/DoubleBindingTextViewFragment.java
@@ -0,0 +1,78 @@
+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.EditText;
+import android.widget.TextView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnTextChanged;
+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 {
+
+ @BindView(R.id.double_binding_num1)
+ EditText _number1;
+
+ @BindView(R.id.double_binding_num2)
+ EditText _number2;
+
+ @BindView(R.id.double_binding_result)
+ TextView _result;
+
+ Disposable _disposable;
+ PublishProcessor _resultEmitterSubject;
+ private Unbinder unbinder;
+
+ @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);
+
+ _resultEmitterSubject = PublishProcessor.create();
+
+ _disposable =
+ _resultEmitterSubject.subscribe(
+ aFloat -> {
+ _result.setText(String.valueOf(aFloat));
+ });
+
+ onNumberChanged();
+ _number2.requestFocus();
+
+ return layout;
+ }
+
+ @OnTextChanged({R.id.double_binding_num1, R.id.double_binding_num2})
+ public void onNumberChanged() {
+ float num1 = 0;
+ float num2 = 0;
+
+ if (!isEmpty(_number1.getText().toString())) {
+ num1 = Float.parseFloat(_number1.getText().toString());
+ }
+
+ 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
new file mode 100644
index 00000000..775feaef
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/ExponentialBackoffFragment.java
@@ -0,0 +1,234 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+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.R;
+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 org.reactivestreams.Publisher;
+import timber.log.Timber;
+
+import static android.os.Looper.getMainLooper;
+
+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);
+ });
+ }
+
+ // -----------------------------------------------------------------------------------
+
+ // 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 !!!
+
+ //public static class RetryWithDelay
+ public class RetryWithDelay implements Function, Publisher>> {
+
+ 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;
+ }
+
+ // 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)
+
+ @Override
+ public Publisher> apply(Flowable extends Throwable> inputObservable) {
+
+ // it is critical to use inputObservable in the chain for the result
+ // ignoring it and doing your own thing will break the sequence
+
+ 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)
+
+ Timber.d("Retrying in %d ms", _retryCount * _retryDelayMillis);
+ _log(String.format("Retrying in %d ms", _retryCount * _retryDelayMillis));
+
+ return Flowable.timer(_retryCount * _retryDelayMillis, TimeUnit.MILLISECONDS);
+ }
+
+ Timber.d("Argh! i give up");
+
+ // 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);
+ }
+ });
+ }
+ }
+}
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
new file mode 100644
index 00000000..efc4c2f8
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/FormValidationCombineLatestFragment.java
@@ -0,0 +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 butterknife.BindView;
+import butterknife.ButterKnife;
+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;
+
+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
new file mode 100644
index 00000000..75f68fa2
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/MainFragment.java
@@ -0,0 +1,139 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+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 {
+
+ 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
new file mode 100644
index 00000000..5eb8b187
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PollingFragment.java
@@ -0,0 +1,237 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.content.Context;
+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 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 org.reactivestreams.Publisher;
+import timber.log.Timber;
+
+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");
+ }
+ _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);
+ });
+ }
+ }
+
+ private void _setupLogger() {
+ _logs = new ArrayList<>();
+ _adapter = new LogAdapter(getActivity(), new ArrayList<>());
+ _logsList.setAdapter(_adapter);
+ _counter = 0;
+ }
+
+ private boolean _isCurrentlyOnMainThread() {
+ return Looper.myLooper() == Looper.getMainLooper();
+ }
+
+ //public static class RepeatWithDelay
+ public class RepeatWithDelay implements Function, Publisher> {
+
+ private final int _repeatLimit;
+ private final int _pollingInterval;
+ private int _repeatCount = 1;
+
+ RepeatWithDelay(int repeatLimit, int pollingInterval) {
+ _pollingInterval = pollingInterval;
+ _repeatLimit = repeatLimit;
+ }
+
+ // this is a notificationhandler, all we care about is
+ // the emission "type" not emission "content"
+ // only onNext triggers a re-subscription
+
+ @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
+
+ 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 {
+
+ 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/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
new file mode 100644
index 00000000..3871437d
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/PseudoCacheMergeFragment.java
@@ -0,0 +1,144 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+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.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 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);
+ }
+
+ return list;
+ }
+
+ private Observable> _getCachedData() {
+
+ List> list = new ArrayList<>();
+
+ Pair dataWithAgePair;
+
+ for (String username : _contributionMap.keySet()) {
+ Contributor c = new Contributor();
+ c.login = username;
+ c.contributions = _contributionMap.get(username);
+
+ dataWithAgePair = new Pair<>(c, System.currentTimeMillis());
+ list.add(dataWithAgePair);
+ }
+
+ 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
new file mode 100644
index 00000000..baa391ed
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitAsyncTaskDeathFragment.java
@@ -0,0 +1,121 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import com.morihacky.android.rxjava.R;
+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 butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+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 {
+
+ @BindView(R.id.btn_demo_retrofit_async_death_username)
+ EditText _username;
+
+ @BindView(R.id.log_list)
+ ListView _resultList;
+
+ private GithubApi _githubService;
+ private ArrayAdapter _adapter;
+ 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);
+ }
+
+ @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);
+ 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();
+ }
+
+ @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]);
+ }
+
+ @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 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
new file mode 100644
index 00000000..0626e5b0
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitFragment.java
@@ -0,0 +1,184 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+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 com.morihacky.android.rxjava.retrofit.User;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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 {
+
+ @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 void onComplete() {
+ 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();
+
+ _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 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");
+ }
+
+ @Override
+ public void onNext(Pair pair) {
+ User user = pair.first;
+ Contributor contributor = pair.second;
+
+ _adapter.add(
+ format(
+ "%s(%s) has made %d contributions to %s",
+ user.name,
+ user.email,
+ contributor.contributions,
+ _repo.getText().toString()));
+
+ _adapter.notifyDataSetChanged();
+
+ 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
new file mode 100644
index 00000000..00882958
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1Fragment.java
@@ -0,0 +1,138 @@
+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;
+import android.support.v4.app.FragmentManager;
+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 butterknife.Unbinder;
+import com.morihacky.android.rxjava.R;
+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 timber.log.Timber;
+
+public class RotationPersist1Fragment extends BaseFragment
+ implements RotationPersist1WorkerFragment.IAmYourMaster {
+
+ public static final String TAG = RotationPersist1Fragment.class.toString();
+
+ @BindView(R.id.list_threading_log)
+ ListView _logList;
+
+ private LogAdapter _adapter;
+ private List _logs;
+ private Unbinder unbinder;
+
+ private CompositeDisposable _disposables = new CompositeDisposable();
+
+ // -----------------------------------------------------------------------------------
+
+ @OnClick(R.id.btn_rotate_persist)
+ public void startOperationFromWorkerFrag() {
+ _logs = new ArrayList<>();
+ _adapter.clear();
+
+ FragmentManager fm = getActivity().getSupportFragmentManager();
+ RotationPersist1WorkerFragment frag =
+ (RotationPersist1WorkerFragment) fm.findFragmentByTag(RotationPersist1WorkerFragment.TAG);
+
+ if (frag == null) {
+ frag = new RotationPersist1WorkerFragment();
+ fm.beginTransaction().add(frag, RotationPersist1WorkerFragment.TAG).commit();
+ } else {
+ Timber.d("Worker frag already spawned");
+ }
+ }
+
+ @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
new file mode 100644
index 00000000..d16adc14
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist1WorkerFragment.java
@@ -0,0 +1,81 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import com.morihacky.android.rxjava.MainActivity;
+import io.reactivex.Flowable;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.flowables.ConnectableFlowable;
+import java.util.concurrent.TimeUnit;
+
+public class RotationPersist1WorkerFragment extends Fragment {
+
+ public static final String TAG = RotationPersist1WorkerFragment.class.toString();
+
+ 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);
+
+ _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);
+
+ // 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
new file mode 100644
index 00000000..6e523013
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2Fragment.java
@@ -0,0 +1,124 @@
+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;
+import android.support.v4.app.FragmentManager;
+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.R;
+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 timber.log.Timber;
+
+public class RotationPersist2Fragment extends BaseFragment
+ implements RotationPersist2WorkerFragment.IAmYourMaster {
+
+ public static final String TAG = RotationPersist2Fragment.class.toString();
+
+ @BindView(R.id.list_threading_log)
+ ListView _logList;
+
+ private LogAdapter _adapter;
+ private List _logs;
+
+ private CompositeDisposable _disposables = new CompositeDisposable();
+
+ // -----------------------------------------------------------------------------------
+
+ @OnClick(R.id.btn_rotate_persist)
+ public void startOperationFromWorkerFrag() {
+ _logs = new ArrayList<>();
+ _adapter.clear();
+
+ FragmentManager fm = getActivity().getSupportFragmentManager();
+ RotationPersist2WorkerFragment frag =
+ (RotationPersist2WorkerFragment) fm.findFragmentByTag(RotationPersist2WorkerFragment.TAG);
+
+ if (frag == null) {
+ frag = new RotationPersist2WorkerFragment();
+ fm.beginTransaction().add(frag, RotationPersist2WorkerFragment.TAG).commit();
+ } else {
+ Timber.d("Worker frag already spawned");
+ }
+ }
+
+ @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
new file mode 100644
index 00000000..cdbe6cd3
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/RotationPersist2WorkerFragment.java
@@ -0,0 +1,80 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import com.morihacky.android.rxjava.MainActivity;
+import io.reactivex.Flowable;
+import io.reactivex.processors.PublishProcessor;
+import java.util.concurrent.TimeUnit;
+
+public class RotationPersist2WorkerFragment extends Fragment {
+
+ public static final String TAG = RotationPersist2WorkerFragment.class.toString();
+
+ private PublishProcessor _intStream;
+ private PublishProcessor _lifeCycleStream;
+
+ 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);
+
+ _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
new file mode 100644
index 00000000..abeace6b
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/TimeoutDemoFragment.java
@@ -0,0 +1,183 @@
+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.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 timber.log.Timber;
+
+public class TimeoutDemoFragment extends BaseFragment {
+
+ @BindView(R.id.list_threading_log)
+ ListView _logsList;
+
+ private LogAdapter _adapter;
+ private DisposableObserver _disposable;
+ private List _logs;
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (_disposable == null) {
+ return;
+ }
+
+ _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 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 Observable extends String> _onTimeoutObservable() {
+ return Observable.create(
+ new ObservableOnSubscribe() {
+
+ @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();
+ }
+}
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
new file mode 100644
index 00000000..55fb027c
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/fragments/TimingDemoFragment.java
@@ -0,0 +1,236 @@
+package com.morihacky.android.rxjava.fragments;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+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.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 timber.log.Timber;
+
+import static android.os.Looper.getMainLooper;
+import static android.os.Looper.myLooper;
+
+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;
+ }
+
+ _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/GithubApi.java b/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubApi.java
index d25a883d..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
@@ -1,22 +1,26 @@
package com.morihacky.android.rxjava.retrofit;
import java.util.List;
-import retrofit.http.GET;
-import retrofit.http.Path;
-import rx.Observable;
+
+import io.reactivex.Observable;
+import retrofit2.http.GET;
+import retrofit2.http.Path;
public interface GithubApi {
- /**
- * See https://developer.github.com/v3/repos/#list-contributors
- */
+ /** 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")
- Observable> contributors(@Path("owner") String owner,
- @Path("repo") String repo);
+ List getContributors(@Path("owner") String owner, @Path("repo") String repo);
- /**
- * See https://developer.github.com/v3/users/
- */
+ /** See https://developer.github.com/v3/users/ */
@GET("/users/{user}")
Observable user(@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
new file mode 100644
index 00000000..3ab57571
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/retrofit/GithubService.java
@@ -0,0 +1,46 @@
+package com.morihacky.android.rxjava.retrofit;
+
+import android.text.TextUtils;
+
+import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+import static java.lang.String.format;
+
+public class GithubService {
+
+ private GithubService() {}
+
+ 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)) {
+
+ OkHttpClient client =
+ new OkHttpClient.Builder()
+ .addInterceptor(
+ chain -> {
+ Request request = chain.request();
+ Request newReq =
+ request
+ .newBuilder()
+ .addHeader("Authorization", format("token %s", githubToken))
+ .build();
+ return chain.proceed(newReq);
+ })
+ .build();
+
+ builder.client(client);
+ }
+
+ return builder.build().create(GithubApi.class);
+ }
+}
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
new file mode 100644
index 00000000..0e8f4fed
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java
@@ -0,0 +1,25 @@
+package com.morihacky.android.rxjava.rxbus;
+
+import com.jakewharton.rxrelay2.PublishRelay;
+import com.jakewharton.rxrelay2.Relay;
+
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+
+/** courtesy: https://gist.github.com/benjchristensen/04eef9ca0851f3a5d7bf */
+public class RxBus {
+
+ private final Relay _bus = PublishRelay.create().toSerialized();
+
+ public void send(Object o) {
+ _bus.accept(o);
+ }
+
+ public Flowable asFlowable() {
+ return _bus.toFlowable(BackpressureStrategy.LATEST);
+ }
+
+ 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
new file mode 100644
index 00000000..f08ea85c
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java
@@ -0,0 +1,37 @@
+package com.morihacky.android.rxjava.rxbus;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import butterknife.ButterKnife;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.fragments.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 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();
+ }
+
+ 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
new file mode 100644
index 00000000..1eb1e2ec
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java
@@ -0,0 +1,66 @@
+package com.morihacky.android.rxjava.rxbus;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.view.ViewCompat;
+import android.view.LayoutInflater;
+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;
+
+public class RxBusDemo_Bottom1Fragment extends BaseFragment {
+
+ @BindView(R.id.demo_rxbus_tap_txt)
+ TextView _tapEventTxtShow;
+
+ 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 void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ _rxBus = ((MainActivity) getActivity()).getRxBusSingleton();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ _disposables = new CompositeDisposable();
+
+ _disposables.add(
+ _rxBus
+ .asFlowable()
+ .subscribe(
+ event -> {
+ if (event instanceof RxBusDemoFragment.TapEvent) {
+ _showTapText();
+ }
+ }));
+ }
+
+ @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);
+ }
+}
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
new file mode 100644
index 00000000..7cb08f4d
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java
@@ -0,0 +1,99 @@
+package com.morihacky.android.rxjava.rxbus;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.view.ViewCompat;
+import android.view.LayoutInflater;
+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;
+
+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
new file mode 100644
index 00000000..d45ec46e
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java
@@ -0,0 +1,99 @@
+package com.morihacky.android.rxjava.rxbus;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.view.ViewCompat;
+import android.view.LayoutInflater;
+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.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.flowables.ConnectableFlowable;
+import java.util.concurrent.TimeUnit;
+
+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
new file mode 100644
index 00000000..7d6c58fb
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java
@@ -0,0 +1,38 @@
+package com.morihacky.android.rxjava.rxbus;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import com.morihacky.android.rxjava.MainActivity;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.fragments.BaseFragment;
+
+public class RxBusDemo_TopFragment extends BaseFragment {
+
+ 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 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());
+ }
+ }
+}
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
new file mode 100644
index 00000000..a6ec55cf
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/volley/MyVolley.java
@@ -0,0 +1,28 @@
+package com.morihacky.android.rxjava.volley;
+
+import android.content.Context;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.Volley;
+
+/**
+ * Helper class that is used to provide references to initialized RequestQueue(s) and ImageLoader(s)
+ */
+public class MyVolley {
+ private static RequestQueue mRequestQueue;
+
+ private MyVolley() {
+ // no instances
+ }
+
+ public static void init(Context context) {
+ mRequestQueue = Volley.newRequestQueue(context);
+ }
+
+ 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
new file mode 100644
index 00000000..aaa55d08
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/volley/VolleyDemoFragment.java
@@ -0,0 +1,180 @@
+package com.morihacky.android.rxjava.volley;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import android.util.Log;
+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.android.volley.Request;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.JsonObjectRequest;
+import com.android.volley.toolbox.RequestFuture;
+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 timber.log.Timber;
+
+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);
+ });
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..cfa108f1
--- /dev/null
+++ b/app/src/main/java/com/morihacky/android/rxjava/wiring/LogAdapter.java
@@ -0,0 +1,13 @@
+package com.morihacky.android.rxjava.wiring;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import com.morihacky.android.rxjava.R;
+import java.util.List;
+
+public 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/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/drawable/btn_round.xml b/app/src/main/res/drawable/btn_round.xml
new file mode 100644
index 00000000..55ff6d2b
--- /dev/null
+++ b/app/src/main/res/drawable/btn_round.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 438a8d65..00000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/app/src/main/res/layout/fragment_buffer.xml b/app/src/main/res/layout/fragment_buffer.xml
index 7839dacd..3464ac5b 100644
--- a/app/src/main/res/layout/fragment_buffer.xml
+++ b/app/src/main/res/layout/fragment_buffer.xml
@@ -1,17 +1,19 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ android:text="@string/msg_demo_buffer"
+ />
+ android:textSize="16sp"
+ android:text="@string/tap_me"
+ />
+ android:layout_width="match_parent"
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_concurrency_schedulers.xml b/app/src/main/res/layout/fragment_concurrency_schedulers.xml
index 5d75665d..9b53fb99 100644
--- a/app/src/main/res/layout/fragment_concurrency_schedulers.xml
+++ b/app/src/main/res/layout/fragment_concurrency_schedulers.xml
@@ -1,22 +1,24 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ android:text="@string/msg_demo_concurrency_schedulers"
+ />
+ android:layout_width="match_parent"
+ >
+ android:text="@string/start_long_operation"
+ />
+ android:layout_marginLeft="20dp"
+ />
+ android:layout_width="match_parent"
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_debounce.xml b/app/src/main/res/layout/fragment_debounce.xml
new file mode 100644
index 00000000..e70a44cf
--- /dev/null
+++ b/app/src/main/res/layout/fragment_debounce.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_demo_timing.xml b/app/src/main/res/layout/fragment_demo_timing.xml
new file mode 100644
index 00000000..0cb4286e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_demo_timing.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_double_binding_textview.xml b/app/src/main/res/layout/fragment_double_binding_textview.xml
index 98fd8fee..4d31e90d 100644
--- a/app/src/main/res/layout/fragment_double_binding_textview.xml
+++ b/app/src/main/res/layout/fragment_double_binding_textview.xml
@@ -1,22 +1,24 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ android:text="@string/msg_demo_doublebinding"
+ />
+ android:layout_marginTop="10dp"
+ >
+ />
+ android:text="@string/plus"
+ />
+ android:text="@string/eight"
+ android:inputType="numberDecimal"
+ />
+ android:text="@string/zero"
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_exponential_backoff.xml b/app/src/main/res/layout/fragment_exponential_backoff.xml
new file mode 100644
index 00000000..2ac1e4fc
--- /dev/null
+++ b/app/src/main/res/layout/fragment_exponential_backoff.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_form_validation_comb_latest.xml b/app/src/main/res/layout/fragment_form_validation_comb_latest.xml
new file mode 100644
index 00000000..c30e7fd3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_form_validation_comb_latest.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
index 8e66e952..6b29ee24 100644
--- a/app/src/main/res/layout/fragment_main.xml
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -1,11 +1,12 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ tools:context=".DemoActivity"
+ >
+ android:text="@string/btn_demo_schedulers"
+ />
+ android:text="@string/btn_demo_buffer"
+ />
+ android:text="@string/btn_demo_debounce"
+ />
+ android:text="@string/btn_demo_retrofit"
+ />
+ android:text="@string/btn_demo_double_binding_textview"
+ />
+ android:text="@string/btn_demo_polling" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_network_detector.xml b/app/src/main/res/layout/fragment_network_detector.xml
new file mode 100644
index 00000000..b825cbf6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_network_detector.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_pagination.xml b/app/src/main/res/layout/fragment_pagination.xml
new file mode 100644
index 00000000..3e449c62
--- /dev/null
+++ b/app/src/main/res/layout/fragment_pagination.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_polling.xml b/app/src/main/res/layout/fragment_polling.xml
index a79b2ebb..9417b8a3 100644
--- a/app/src/main/res/layout/fragment_polling.xml
+++ b/app/src/main/res/layout/fragment_polling.xml
@@ -1,42 +1,46 @@
-
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ >
+ android:padding="10dp"
+ android:text="@string/msg_demo_polling"
+ />
+ android:orientation="vertical"
+ >
+ />
+ />
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_pseudo_cache.xml b/app/src/main/res/layout/fragment_pseudo_cache.xml
new file mode 100644
index 00000000..ed7de8ee
--- /dev/null
+++ b/app/src/main/res/layout/fragment_pseudo_cache.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_pseudo_cache_concat.xml b/app/src/main/res/layout/fragment_pseudo_cache_concat.xml
new file mode 100644
index 00000000..e5f39896
--- /dev/null
+++ b/app/src/main/res/layout/fragment_pseudo_cache_concat.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_retrofit.xml b/app/src/main/res/layout/fragment_retrofit.xml
index 17a67d2d..557cc6ae 100644
--- a/app/src/main/res/layout/fragment_retrofit.xml
+++ b/app/src/main/res/layout/fragment_retrofit.xml
@@ -1,21 +1,23 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ android:text="@string/msg_demo_retrofit"
+ />
+ android:layout_width="match_parent"
+ >
+ android:text="@string/log_contributors_of"
+ />
+ android:text="@string/square"
+ android:hint="@string/owner"
+ />
+ android:text="@string/retrofit"
+ android:hint="@string/reponame"
+ />
+
+ android:layout_width="match_parent"
+ >
+ android:text="@string/log_with_full_user_info"
+ />
+ android:text="@string/square"
+ android:hint="@string/owner"
+ />
+ android:text="@string/retrofit"
+ android:hint="@string/reponame"
+ />
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_retrofit_async_task_death.xml b/app/src/main/res/layout/fragment_retrofit_async_task_death.xml
new file mode 100644
index 00000000..8fd1ffe6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_retrofit_async_task_death.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_subject_debounce.xml b/app/src/main/res/layout/fragment_rotation_persist.xml
similarity index 63%
rename from app/src/main/res/layout/fragment_subject_debounce.xml
rename to app/src/main/res/layout/fragment_rotation_persist.xml
index 25485839..67bc0c67 100644
--- a/app/src/main/res/layout/fragment_subject_debounce.xml
+++ b/app/src/main/res/layout/fragment_rotation_persist.xml
@@ -1,27 +1,29 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ android:text="@string/msg_demo_rotation_persist"
+ />
-
+ android:text="@string/start_operation"
+ />
+ android:layout_width="match_parent"
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_rxbus_bottom.xml b/app/src/main/res/layout/fragment_rxbus_bottom.xml
new file mode 100644
index 00000000..9bdbdea8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_rxbus_bottom.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_rxbus_demo.xml b/app/src/main/res/layout/fragment_rxbus_demo.xml
new file mode 100644
index 00000000..80df9875
--- /dev/null
+++ b/app/src/main/res/layout/fragment_rxbus_demo.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_rxbus_frag3.xml b/app/src/main/res/layout/fragment_rxbus_frag3.xml
new file mode 100644
index 00000000..5ba6621b
--- /dev/null
+++ b/app/src/main/res/layout/fragment_rxbus_frag3.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_rxbus_top.xml b/app/src/main/res/layout/fragment_rxbus_top.xml
new file mode 100644
index 00000000..58a17ef5
--- /dev/null
+++ b/app/src/main/res/layout/fragment_rxbus_top.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_subject_timeout.xml b/app/src/main/res/layout/fragment_subject_timeout.xml
index 1a4e9cf8..3d4c6d0b 100644
--- a/app/src/main/res/layout/fragment_subject_timeout.xml
+++ b/app/src/main/res/layout/fragment_subject_timeout.xml
@@ -1,40 +1,45 @@
-
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ android:text="@string/msg_demo_timeout"
+ />
+ android:layout_width="match_parent"
+ >
+ android:text="@string/button_1"
+ />
+ android:text="@string/button_2"
+ />
+ android:layout_width="match_parent"
+ />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_timer_demo.xml b/app/src/main/res/layout/fragment_timer_demo.xml
new file mode 100644
index 00000000..feadc0f3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_timer_demo.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_volley.xml b/app/src/main/res/layout/fragment_volley.xml
new file mode 100644
index 00000000..fb2aea9e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_volley.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_btn.xml b/app/src/main/res/layout/item_btn.xml
new file mode 100644
index 00000000..a7a03b36
--- /dev/null
+++ b/app/src/main/res/layout/item_btn.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_log.xml b/app/src/main/res/layout/item_log.xml
index e1e27d78..d0128694 100644
--- a/app/src/main/res/layout/item_log.xml
+++ b/app/src/main/res/layout/item_log.xml
@@ -1,9 +1,13 @@
-
\ No newline at end of file
+ android:layout_height="wrap_content"
+ android:paddingEnd="@dimen/activity_horizontal_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingStart="@dimen/activity_horizontal_margin"
+ android:textSize="@dimen/text_medium"
+ tools:text="Item Log goes here"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_log_white.xml b/app/src/main/res/layout/item_log_white.xml
new file mode 100644
index 00000000..08bebb01
--- /dev/null
+++ b/app/src/main/res/layout/item_log_white.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/demo.xml b/app/src/main/res/menu/demo.xml
index dd9b16cd..d5726166 100644
--- a/app/src/main/res/menu/demo.xml
+++ b/app/src/main/res/menu/demo.xml
@@ -1,8 +1,11 @@
- -
+
+ android:showAsAction="never"
+ />
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..c70a932a
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,12 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
+ #eb676b
+ #488cef
+ #3bc4a2
+ #cfcfcf
+ #fff
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 47c82246..1fcc661d 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,4 +2,13 @@
16dp
16dp
+
+ 10sp
+ 12sp
+ 14sp
+ 16sp
+ 20sp
+ 24sp
+ 60sp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4c20c795..6fabe007 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,20 +4,100 @@
Android-RxJava
Hello world!
Settings
-
- 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! \n\n To really see this 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
- This is a demo of how events can be accumulated using the "buffer" operation. Tap the button below repetitively and you will notice in the logs that button taps are collected over a span of 2s and printed below.
- As you type in the input box, it will not shoot out log messages at every single input character change, but rather only pick the lastly emitted event (i.e. input) and log that. \n\nThis is the debounce/throttleWithTimeout method in RxJava.
- This is a demo of terminating events, that take too long to process. Events in this demo should timeout in 3 seconds. Button 1 is an event that takes 2s to process, and Button 2 is a n event that takes 5s to process. Hit Button 2, and see in the logs that it\'s been cancelled, while this is not the case for Button 1.
- Retrofit from Square is a super easy networking helper library. It works really well with RxJava and these are examples taken from Jake Wharton\'s talk at Netflix (see README). Really the only interesting bits here are in the code and logs.
- Watch how the result gloriously auto-updates based on your changing inputs. Using a technique like this, you could achieve the two-way binding in Angular Js, or more efficiently use a pattern like the Presentation View Model.
- This is demo of polling or making a call repeatedly with RxJava. \n\nSimple polling: Notice in the logs how a network call (simulated) is repeatedly made in the background.
+
bg work (schedulers & concurrency)
accumulate calls (buffer)
- search text listener(subject debouncing)
- delayed jobs (timeout)
+ search text listener(debounce)
+ Timeout long running jobs
Retrofit + RxJava
Double binding (PublishSubject)
Polling with RxJava
+ Event Bus with RxJava
+ Form Validation with CombineLatest
+ Pseudo cache using concat
+ Variations of timing/intervals/delays
+ Exponential backoff
+ Rotation persist
+ Volley request demo
+ Paging example
+ MOAR
+ Network Detector (Subject)
+ Setup & teardown resources (using)
+ MultiConnect operator playground
+
+ 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
+ This is a Volley request demo
+ 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! \n\n To really see this 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
+ This is a demo of how events can be accumulated using the "buffer" operation. Tap the button below repetitively and you will notice in the logs that button taps are collected over a span of 2s and printed below.
+ As you type in the input box, it will not shoot out log messages at every single input character change, but rather only pick the last one.
+ This is a demo of terminating events, that take too long to process. Events in this demo should timeout in 3 seconds. Button 1 is an event that takes 2s to process, and Button 2 is a n event that takes 5s to process. Hit Button 2, and see in the logs that it\'s been cancelled, while this is not the case for Button 1.
+ Retrofit from Square is a super easy networking helper library. It works really well with RxJava and these are examples taken from Jake Wharton\'s talk at Netflix (see README). Really the only interesting bits here are in the code and logs.
+ This shows how you can replace an AsyncTask with RxJava. The interesting parts are in the code.
+ Watch how the result gloriously auto-updates sed on your changing inputs. Using a technique like this, you could achieve the two-way binding in Angular Js, or more efficiently use a pattern like the Presentation View Model.
+ Demo polling or making a call repeatedly with RxJava.\n\nSimple polling: Notice in the logs how a (simulated) network call is repeatedly made in the background.
+ These two examples demonstrate retrying and executing with a delay using an exponential backoff strategy.
+ Tap on the below button and RxBus will listen to the events
+ Monitor the state of multiple observables with the combineLatest operator. Only after all the 3 inputs contain valid entries will the submit button light up
+ 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
+ This is an example of starting an Observable and using the result across rotations. There are many ways to do this, we use ViewModels from architecture components
+ This is a demo of how to use Subjects to detect Network connectivity\nToggle your Wifi/Network on or off and notice the logs
+ This is a demo of the somewhat unknown operator "using".\n\nYou 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.
+ 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.
+ 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)
+ 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)
+ 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)
+ 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)
+
+ Concat merges the results sequentially. But notice that the latter subscription starts only AFTER the first one completes. Some unnecessary waiting there.
+ Concat eager is cooler. Both subscriptions start at the same time (parallely) but the order of emission is respected.
+ Merge presents the result as they come in which is great if the disk is *always* faster. If not, you have problems.
+ Here\'s a fake example where the disk is made slower than the network call. You can see the results are whack.
+ This is an optimized merge and probably want to use. Notice subscriptions happen immediately and network results are respected regardless of when they come in. So if the disk is slower, it is discarded.
+ Similar to optimized merge (same code). Notice though that if the disk is slower it\'s discarded in favor of the "fresh" network data which in this case happens to be faster.
+ tap me
+ Start long operation
+ Enter some search text
+ BTN 1
+ BTN 2
+ BTN 3
+ BTN 4
+ BTN 5
+ CLEAR LOG
+ 100
+ +
+ 8
+ 0
+ Retry
+ Delay
+ Enter a valid email below:
+ than 8 chrs):]]>
+
+ Submit
+ Start simple polling
+ Start increasingly delayed polling
+ Info about the demo will show up here
+ concat
+ concat (eager)
+ merge
+ merge (slower disk)
+ merge (optimized)
+ merge (optimized) slow disk
+ network call]]>
+ Log contributors of:
+ square
+ owner
+ retrofit
+ reponame
+ Log with full User Info:
+ Log User Info:
+ kaushikgopal
+ Start operation
+ 1
+ tap!
+ Button 1
+ Button 2
+ Start
+ I AMZ BTN
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index ff6c9d2c..a11c0b30 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,8 +1,11 @@
-
diff --git a/build.gradle b/build.gradle
index 7744ab79..b8b0e3dd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,8 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:0.12.+'
+ 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
// in the individual module build.gradle files
@@ -16,5 +17,23 @@ buildscript {
allprojects {
repositories {
jcenter()
+ maven { url 'https://maven.google.com' }
+ maven {
+ url "https://jitpack.io"
+ }
+ }
+
+ ext {
+ sdkVersion = 24
+ buildToolsVrs = "25.0.0"
+
+ kotlinVersion = "1.1.3-2"
+
+ archComponentsVersion = "1.0.0-alpha3"
+ butterKnifeVersion = '8.5.1'
+ mockitoKotlinVersion = "1.4.0"
+ okhttpVersion = "3.0.1"
+ retrofitVersion = "2.0.0"
+ supportLibVersion = "25.3.1"
}
}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 8c0fb64a..2f220cae 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1e61d1fd..391ac683 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Apr 10 15:27:10 PDT 2013
+#Wed May 24 09:29:38 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip
diff --git a/gradlew b/gradlew
index 91a7e269..4453ccea 100755
--- a/gradlew
+++ b/gradlew
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
##############################################################################
##
@@ -6,12 +6,30 @@
##
##############################################################################
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -30,6 +48,7 @@ die ( ) {
cygwin=false
msys=false
darwin=false
+nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
@@ -40,31 +59,11 @@ case "`uname`" in
MINGW* )
msys=true
;;
+ NONSTOP* )
+ nonstop=true
+ ;;
esac
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -90,7 +89,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -114,6 +113,7 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,11 +154,19 @@ if $cygwin ; then
esac
fi
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
+# Escape application args
+save ( ) {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index aec99730..e95643d6 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,10 +46,9 @@ echo location of your Java installation.
goto fail
:init
-@rem Get command-line arguments, handling Windowz variants
+@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line