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
index afaa595c..0e8f4fed 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBus.java
@@ -1,30 +1,25 @@
package com.morihacky.android.rxjava.rxbus;
-import rx.Observable;
-import rx.subjects.PublishSubject;
-import rx.subjects.SerializedSubject;
-import rx.subjects.Subject;
+import com.jakewharton.rxrelay2.PublishRelay;
+import com.jakewharton.rxrelay2.Relay;
-/**
- * courtesy: https://gist.github.com/benjchristensen/04eef9ca0851f3a5d7bf
- */
-public class RxBus {
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
- //private final PublishSubject _bus = PublishSubject.create();
+/** courtesy: https://gist.github.com/benjchristensen/04eef9ca0851f3a5d7bf */
+public class RxBus {
- // If multiple threads are going to emit events to this
- // then it must be made thread-safe like this instead
- private final Subject _bus = new SerializedSubject<>(PublishSubject.create());
+ private final Relay _bus = PublishRelay.create().toSerialized();
public void send(Object o) {
- _bus.onNext(o);
+ _bus.accept(o);
}
- public Observable toObserverable() {
- return _bus;
+ public Flowable asFlowable() {
+ return _bus.toFlowable(BackpressureStrategy.LATEST);
}
public boolean hasObservers() {
return _bus.hasObservers();
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java
index d9dd4c10..f08ea85c 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemoFragment.java
@@ -2,22 +2,20 @@
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 butterknife.ButterKnife;
-import com.morihacky.android.rxjava.app.R;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.fragments.BaseFragment;
-public class RxBusDemoFragment
- extends Fragment {
+public class RxBusDemoFragment extends BaseFragment {
@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_rxbus_demo, container, false);
- ButterKnife.inject(this, layout);
+ ButterKnife.bind(this, layout);
return layout;
}
@@ -25,7 +23,8 @@ public View onCreateView(LayoutInflater inflater,
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- getActivity().getSupportFragmentManager()
+ getActivity()
+ .getSupportFragmentManager()
.beginTransaction()
.replace(R.id.demo_rxbus_frag_1, new RxBusDemo_TopFragment())
.replace(R.id.demo_rxbus_frag_2, new RxBusDemo_Bottom3Fragment())
@@ -35,4 +34,4 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
}
public static class TapEvent {}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java
index 0cc5b5ea..1eb1e2ec 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom1Fragment.java
@@ -2,35 +2,31 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
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 butterknife.InjectView;
import com.morihacky.android.rxjava.MainActivity;
-import com.morihacky.android.rxjava.app.R;
-import rx.functions.Action1;
-import rx.subscriptions.CompositeSubscription;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.fragments.BaseFragment;
+import io.reactivex.disposables.CompositeDisposable;
-import static rx.android.observables.AndroidObservable.bindFragment;
+public class RxBusDemo_Bottom1Fragment extends BaseFragment {
-public class RxBusDemo_Bottom1Fragment
- extends Fragment {
+ @BindView(R.id.demo_rxbus_tap_txt)
+ TextView _tapEventTxtShow;
+ private CompositeDisposable _disposables;
private RxBus _rxBus;
- private CompositeSubscription _subscriptions;
-
- @InjectView(R.id.demo_rxbus_tap_txt) TextView _tapEventTxtShow;
@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_rxbus_bottom, container, false);
- ButterKnife.inject(this, layout);
+ ButterKnife.bind(this, layout);
return layout;
}
@@ -43,18 +39,23 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
@Override
public void onStart() {
super.onStart();
- _subscriptions = new CompositeSubscription();
+ _disposables = new CompositeDisposable();
+
+ _disposables.add(
+ _rxBus
+ .asFlowable()
+ .subscribe(
+ event -> {
+ if (event instanceof RxBusDemoFragment.TapEvent) {
+ _showTapText();
+ }
+ }));
+ }
- _subscriptions//
- .add(bindFragment(this, _rxBus.toObserverable())//
- .subscribe(new Action1() {
- @Override
- public void call(Object event) {
- if (event instanceof RxBusDemoFragment.TapEvent) {
- _showTapText();
- }
- }
- }));
+ @Override
+ public void onStop() {
+ super.onStop();
+ _disposables.clear();
}
private void _showTapText() {
@@ -62,10 +63,4 @@ private void _showTapText() {
_tapEventTxtShow.setAlpha(1f);
ViewCompat.animate(_tapEventTxtShow).alphaBy(-1f).setDuration(400);
}
-
- @Override
- public void onStop() {
- super.onStop();
- _subscriptions.unsubscribe();
- }
}
diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java
index 3b159de0..7cb08f4d 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom2Fragment.java
@@ -2,40 +2,38 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
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 butterknife.InjectView;
import com.morihacky.android.rxjava.MainActivity;
-import com.morihacky.android.rxjava.app.R;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.fragments.BaseFragment;
+import io.reactivex.Flowable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import rx.Observable;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.functions.Action1;
-import rx.subscriptions.CompositeSubscription;
-import static rx.android.observables.AndroidObservable.bindFragment;
+public class RxBusDemo_Bottom2Fragment extends BaseFragment {
-public class RxBusDemo_Bottom2Fragment
- extends Fragment {
+ @BindView(R.id.demo_rxbus_tap_txt)
+ TextView _tapEventTxtShow;
- private RxBus _rxBus;
- private CompositeSubscription _subscriptions;
+ @BindView(R.id.demo_rxbus_tap_count)
+ TextView _tapEventCountShow;
- @InjectView(R.id.demo_rxbus_tap_txt) TextView _tapEventTxtShow;
- @InjectView(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) {
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false);
- ButterKnife.inject(this, layout);
+ ButterKnife.bind(this, layout);
return layout;
}
@@ -48,39 +46,34 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
@Override
public void onStart() {
super.onStart();
- _subscriptions = new CompositeSubscription();
-
- Observable tapEventEmitter = _rxBus.toObserverable().share();
-
- _subscriptions//
- .add(bindFragment(this, tapEventEmitter)//
- .subscribe(new Action1() {
- @Override
- public void call(Object event) {
- if (event instanceof RxBusDemoFragment.TapEvent) {
- _showTapText();
- }
- }
- }));
-
- Observable debouncedEmitter = tapEventEmitter.debounce(1, TimeUnit.SECONDS);
- Observable> debouncedBufferEmitter = tapEventEmitter.buffer(debouncedEmitter);
-
- _subscriptions//
- .add(debouncedBufferEmitter//
- .observeOn(AndroidSchedulers.mainThread())//
- .subscribe(new Action1>() {
- @Override
- public void call(List taps) {
- _showTapCount(taps.size());
- }
- }));
+ _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();
- _subscriptions.unsubscribe();
+ _disposables.clear();
}
// -----------------------------------------------------------------------------------
diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java
index edad963d..d45ec46e 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_Bottom3Fragment.java
@@ -2,42 +2,37 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
-import android.support.v4.app.Fragment;
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 butterknife.InjectView;
import com.morihacky.android.rxjava.MainActivity;
-import com.morihacky.android.rxjava.app.R;
-import java.util.List;
+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;
-import rx.Observable;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.functions.Action1;
-import rx.functions.Func1;
-import rx.observables.ConnectableObservable;
-import rx.subscriptions.CompositeSubscription;
-import static rx.android.observables.AndroidObservable.bindFragment;
+public class RxBusDemo_Bottom3Fragment extends BaseFragment {
-public class RxBusDemo_Bottom3Fragment
- extends Fragment {
+ @BindView(R.id.demo_rxbus_tap_txt)
+ TextView _tapEventTxtShow;
- private RxBus _rxBus;
- private CompositeSubscription _subscriptions;
+ @BindView(R.id.demo_rxbus_tap_count)
+ TextView _tapEventCountShow;
- @InjectView(R.id.demo_rxbus_tap_txt) TextView _tapEventTxtShow;
- @InjectView(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) {
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_rxbus_bottom, container, false);
- ButterKnife.inject(this, layout);
+ ButterKnife.bind(this, layout);
return layout;
}
@@ -50,41 +45,35 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
@Override
public void onStart() {
super.onStart();
- _subscriptions = new CompositeSubscription();
-
- ConnectableObservable tapEventEmitter = _rxBus.toObserverable().publish();
-
- _subscriptions//
- .add(bindFragment(this, tapEventEmitter)//
- .subscribe(new Action1() {
- @Override
- public void call(Object event) {
- if (event instanceof RxBusDemoFragment.TapEvent) {
- _showTapText();
- }
- }
- }));
-
- _subscriptions//
- .add(tapEventEmitter.publish(new Func1, Observable>>() {
- @Override
- public Observable> call(Observable stream) {
- return stream.buffer(stream.debounce(1, TimeUnit.SECONDS));
- }
- }).observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1>() {
- @Override
- public void call(List taps) {
- _showTapCount(taps.size());
- }
- }));
-
- _subscriptions.add(tapEventEmitter.connect());
+ _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();
- _subscriptions.clear();
+ _disposables.clear();
}
// -----------------------------------------------------------------------------------
diff --git a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java
index 53a70434..7d6c58fb 100644
--- a/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java
+++ b/app/src/main/java/com/morihacky/android/rxjava/rxbus/RxBusDemo_TopFragment.java
@@ -2,26 +2,24 @@
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 butterknife.ButterKnife;
import butterknife.OnClick;
import com.morihacky.android.rxjava.MainActivity;
-import com.morihacky.android.rxjava.app.R;
+import com.morihacky.android.rxjava.R;
+import com.morihacky.android.rxjava.fragments.BaseFragment;
-public class RxBusDemo_TopFragment
- extends Fragment {
+public class RxBusDemo_TopFragment extends BaseFragment {
private RxBus _rxBus;
@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_rxbus_top, container, false);
- ButterKnife.inject(this, layout);
+ ButterKnife.bind(this, layout);
return layout;
}
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
index ac7ba8f8..55ff6d2b 100644
--- a/app/src/main/res/drawable/btn_round.xml
+++ b/app/src/main/res/drawable/btn_round.xml
@@ -1,9 +1,12 @@
-
-
+ android:shape="rectangle"
+ >
+
+
\ 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"
+ >