diff --git a/.gitignore b/.gitignore index c6cbe56..067f8ba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ .DS_Store /build /captures + +\.idea/ diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index ed1e2cd..0000000 Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 30aa626..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index ec47cae..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 199f869..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index abddc1e..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7d9f0df..3ece485 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,14 +1,13 @@ apply plugin: 'com.android.application' -apply plugin: 'me.tatarka.retrolambda' android { - compileSdkVersion 25 + compileSdkVersion 27 buildToolsVersion '27.0.3' defaultConfig { applicationId "com.motondon.rxjavademoapp" minSdkVersion 21 - targetSdkVersion 25 + targetSdkVersion 27 versionCode 1 versionName "1.0" @@ -30,17 +29,14 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:25.3.1' - compile 'com.android.support:design:25.3.1' - compile 'com.android.support:recyclerview-v7:25.3.1' - compile 'com.android.support:cardview-v7:25.3.1' + compile 'com.android.support:appcompat-v7:27.1.1' + compile 'com.android.support:design:27.1.1' + compile 'com.android.support:recyclerview-v7:27.1.1' + compile 'com.android.support:cardview-v7:27.1.1' // RxJava dependency - compile 'io.reactivex:rxjava:1.1.9' - - // RxAndroid dependency - compile 'io.reactivex:rxandroid:1.2.1' - + implementation "io.reactivex.rxjava2:rxjava:2.1.13" + implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' // RxBindings dependency compile 'com.jakewharton.rxbinding:rxbinding:0.4.0' diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/CategoryItemsAdapter.java b/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/CategoryItemsAdapter.java index 99fbbfe..6e1ff1a 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/CategoryItemsAdapter.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/CategoryItemsAdapter.java @@ -7,60 +7,58 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.main.CategoryItem; - import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; - public class CategoryItemsAdapter extends RecyclerView.Adapter { - private Context mContext; - private List mCategoryItemList; + private Context mContext; + private List mCategoryItemList; - public CategoryItemsAdapter(Context context, List categoryItemList) { - mContext = context; - mCategoryItemList = categoryItemList; - } + public CategoryItemsAdapter(Context context, List categoryItemList) { + mContext = context; + mCategoryItemList = categoryItemList; + } - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater - .from(mContext) - .inflate(R.layout.item_category_list, parent, false); - return new ViewHolder(v); - } + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(mContext).inflate(R.layout.item_category_list, parent, false); + return new ViewHolder(v); + } - @Override - public void onBindViewHolder(final ViewHolder holder, final int position) { - holder.mCategoryItemName.setText(mCategoryItemList.get(position).mExampleName); - holder.mCategoryItemDetails.setText(mCategoryItemList.get(position).mExampleDetails); + @Override + public void onBindViewHolder(final ViewHolder holder, final int position) { + holder.mCategoryItemName.setText(mCategoryItemList.get(position).mExampleName); + holder.mCategoryItemDetails.setText(mCategoryItemList.get(position).mExampleDetails); - // When user clicks on an example, extract a class that implements that example and call it by using an intent. - holder.itemView.setOnClickListener((v) -> { - Intent exampleIntent = new Intent(mContext, mCategoryItemList.get(position).mExampleActivityClass); - exampleIntent.putExtra("TITLE", mCategoryItemList.get(position).mExampleName); + // When user clicks on an example, extract a class that implements that example and call it by using an intent. + holder.itemView.setOnClickListener((v) -> { + Intent exampleIntent = + new Intent(mContext, mCategoryItemList.get(position).mExampleActivityClass); + exampleIntent.putExtra("TITLE", mCategoryItemList.get(position).mExampleName); - mContext.startActivity(exampleIntent); - }); - } + mContext.startActivity(exampleIntent); + }); + } - @Override - public int getItemCount() { - return mCategoryItemList.size(); - } + @Override + public int getItemCount() { + return mCategoryItemList.size(); + } - public static class ViewHolder extends RecyclerView.ViewHolder { + public static class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.item) TextView mCategoryItemName; - @BindView(R.id.item_details) TextView mCategoryItemDetails; + @BindView(R.id.item) + TextView mCategoryItemName; + @BindView(R.id.item_details) + TextView mCategoryItemDetails; - public ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - } + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/MainActivityAdapter.java b/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/MainActivityAdapter.java index f62f299..737eefa 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/MainActivityAdapter.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/MainActivityAdapter.java @@ -7,70 +7,74 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.main.ExampleByCategoryActivity; import com.motondon.rxjavademoapp.view.main.CategoryItem; +import com.motondon.rxjavademoapp.view.main.ExampleByCategoryActivity; import com.motondon.rxjavademoapp.view.main.MainActivity; - import java.util.ArrayList; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; - public class MainActivityAdapter extends RecyclerView.Adapter { - private List, Pair>> mExampleCategoriessList = new ArrayList<>(); - private MainActivity mMainActivity; + private List, Pair>> mExampleCategoriessList = + new ArrayList<>(); + private MainActivity mMainActivity; - public MainActivityAdapter(MainActivity mainActivity, List, Pair>> mExampleCategoriessList) { - this.mExampleCategoriessList = mExampleCategoriessList; - this.mMainActivity = mainActivity; - } + public MainActivityAdapter(MainActivity mainActivity, + List, Pair>> mExampleCategoriessList) { + this.mExampleCategoriessList = mExampleCategoriessList; + this.mMainActivity = mainActivity; + } - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_cardview_options, parent, false); - return new ViewHolder(view); - } + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_cardview_options, parent, false); + return new ViewHolder(view); + } - @Override - public void onBindViewHolder(final ViewHolder holder, int position) { + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { - // Not too much to say here. We just get the item and set a listener for it. When user clicks on it, he will be redirected - // to the activity that implements the selected example. - Pair, Pair> categoriesDetailsList = mExampleCategoriessList.get(position); + // Not too much to say here. We just get the item and set a listener for it. When user clicks on it, he will be redirected + // to the activity that implements the selected example. + Pair, Pair> categoriesDetailsList = + mExampleCategoriessList.get(position); - final List categoryItemList = categoriesDetailsList.first; - Pair item = categoriesDetailsList.second; + final List categoryItemList = categoriesDetailsList.first; + Pair item = categoriesDetailsList.second; - holder.categoryName.setText(item.first); - holder.categoryDetails.setText(item.second); + holder.categoryName.setText(item.first); + holder.categoryDetails.setText(item.second); - holder.itemView.setOnClickListener((v) -> { - Intent exampleByCategoryIntent = new Intent(mMainActivity.getApplicationContext(), ExampleByCategoryActivity.class); + holder.itemView.setOnClickListener((v) -> { + Intent exampleByCategoryIntent = + new Intent(mMainActivity.getApplicationContext(), ExampleByCategoryActivity.class); - exampleByCategoryIntent.putExtra("CATEGORY_ITEMS",(ArrayList) categoryItemList); - exampleByCategoryIntent.putExtra("TITLE", holder.categoryName.getText().toString()); + exampleByCategoryIntent.putExtra("CATEGORY_ITEMS", (ArrayList) categoryItemList); + exampleByCategoryIntent.putExtra("TITLE", holder.categoryName.getText().toString()); - mMainActivity.startActivity(exampleByCategoryIntent); - }); - } + mMainActivity.startActivity(exampleByCategoryIntent); + }); + } - @Override - public int getItemCount() { - return mExampleCategoriessList.size(); - } + @Override + public int getItemCount() { + return mExampleCategoriessList.size(); + } - public class ViewHolder extends RecyclerView.ViewHolder { + public class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.category_name) TextView categoryName; - @BindView(R.id.category_details) TextView categoryDetails; + @BindView(R.id.category_name) + TextView categoryName; + @BindView(R.id.category_details) + TextView categoryDetails; - public ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - } + public ViewHolder(View itemView) { + super(itemView); + ButterKnife.bind(this, itemView); } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/SimpleStringAdapter.java b/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/SimpleStringAdapter.java index 0ffe12e..604ef85 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/SimpleStringAdapter.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/adapter/SimpleStringAdapter.java @@ -7,72 +7,71 @@ import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.motondon.rxjavademoapp.R; - import java.util.ArrayList; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; - /** * Adapter used to map a String to a text view. */ public class SimpleStringAdapter extends RecyclerView.Adapter { - private final Context mContext; - private final List mStrings = new ArrayList<>(); - - public SimpleStringAdapter(Context context) { - mContext = context; - } - - public void setStrings(List newStrings) { - mStrings.clear(); - mStrings.addAll(newStrings); - notifyDataSetChanged(); - } - - public void addString(String newString) { - mStrings.add(newString); - notifyDataSetChanged(); - } - - public void clear() { - mStrings.clear(); - notifyDataSetChanged(); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_single, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(ViewHolder holder, final int position) { - holder.mItem.setText(mStrings.get(position)); - holder.itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Toast.makeText(mContext, mStrings.get(position), Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - public int getItemCount() { - return mStrings.size(); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - - @BindView(R.id.item) TextView mItem; - - public ViewHolder(View view) { - super(view); - ButterKnife.bind(this, view); - } + private final Context mContext; + private final List mStrings = new ArrayList<>(); + + public SimpleStringAdapter(Context context) { + mContext = context; + } + + public void setStrings(List newStrings) { + mStrings.clear(); + mStrings.addAll(newStrings); + notifyDataSetChanged(); + } + + public void addString(String newString) { + mStrings.add(newString); + notifyDataSetChanged(); + } + + public void clear() { + mStrings.clear(); + notifyDataSetChanged(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = + LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list_single, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, final int position) { + holder.mItem.setText(mStrings.get(position)); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(mContext, mStrings.get(position), Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public int getItemCount() { + return mStrings.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.item) + TextView mItem; + + public ViewHolder(View view) { + super(view); + ButterKnife.bind(this, view); } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureBasicExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureBasicExampleActivity.java index ce9eef9..4e5a6b6 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureBasicExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureBasicExampleActivity.java @@ -6,15 +6,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.BaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.BaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscriber; @@ -22,152 +19,146 @@ import rx.android.schedulers.AndroidSchedulers; /** - * * This activity shows two examples: one that emits items faster than they can be consumed. Quickly it will finish with a MissingBackpressureException. * The second one adds the throttleLast() operator to the chain in order to try to alliviate emitted items downstream to try to avoid that exception. - * */ public class BackpressureBasicExampleActivity extends BaseActivity { - private static final String TAG = BackpressureBasicExampleActivity.class.getSimpleName(); + private static final String TAG = BackpressureBasicExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backpressure_basic_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_backpressure_basic_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_missingbackpressureexception_test) + public void onMissingBackPressureExceptionButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onMissingBackPressureExceptionButtonClick()"); + resetData(); + subscription = starMissingBackpressureExceptionTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - @OnClick(R.id.btn_missingbackpressureexception_test) - public void onMissingBackPressureExceptionButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onMissingBackPressureExceptionButtonClick()"); - resetData(); - subscription = starMissingBackpressureExceptionTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } + } + + @OnClick(R.id.btn_throttle_operator_test) + public void onThrottleOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onThrottleOperatorButtonClick()"); + resetData(); + tvEmittedNumbers.setText("Check the logs to see all emitted items"); + startThrottleOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - @OnClick(R.id.btn_throttle_operator_test) - public void onThrottleOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onThrottleOperatorButtonClick()"); - resetData(); - tvEmittedNumbers.setText("Check the logs to see all emitted items"); - startThrottleOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); + } + + /** + * This example will throw a MissingBackPressureException since the Observable is emitting items much faster + * than they can be consumed. + */ + private Subscription starMissingBackpressureExceptionTest() { + + // Emit one item per millisecond... + return Observable.interval(1, TimeUnit.MILLISECONDS) + + .doOnNext((number) -> { + Log.v(TAG, "doOnNext() - Emitted number: " + number); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + .compose(applySchedulers()) + + // Sleep for 100ms for each emitted item. This will make we receive a BackpressureMissingException quickly. + .subscribe(resultSubscriber(100)); + } + + /** + * By using throttleLast operator (which is pretty much similar to collect) we reduce the chances to get a + * MissingBackpressureException, since it will only emit the last item emitted in a certain period of time. + * + * For this example we are using throttleLast intervalDuration to 100ms, which might be enough to let observer to + * process all emitted items. If we change intervalDuration to a small value (e.g.: 10ms), although we will still + * use throttleLast operator, it will not be enough to prevent buffer's capacity to be full. Try both values to see + * that in action. + */ + private Subscription startThrottleOperatorTest() { + + return Observable.interval(1, TimeUnit.MILLISECONDS) + + .doOnNext((number) -> { + Log.v(TAG, "doOnNext() - Emitted number: " + number); + + // For this example we will not print emitted items on the GUI, since it would freeze it. Check the logs to see all emitted items + // final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + // w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + // Using throttleLast intervalDuration equals to 100ms, we will probably not end up in an exception, since our subscriber will be able to + // process all emitted items accordingly. If we change it to 10ms, it will quickly throw a MissingBackpressureException. + .throttleLast(100, TimeUnit.MILLISECONDS) + + // Just for log purpose + .compose(showDebugMessages("throttleLast(100)")) + + // Just adding some boundaries here + .take(20) + + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(100)); + } + + @NonNull + private Subscriber resultSubscriber(final Integer timeToSleep) { + return new Subscriber() { + + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + @Override + public void onError(final Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e); + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError" + e)); + } + + @Override + public void onNext(final Long number) { + Log.v(TAG, "subscribe.onNext" + " " + number); + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " " + number)); + + try { + Thread.sleep(timeToSleep); + } catch (InterruptedException e) { + Log.v(TAG, "subscribe.onNext. We got a InterruptedException!"); } - } - - /** - * This example will throw a MissingBackPressureException since the Observable is emitting items much faster - * than they can be consumed. - * - * @return - */ - private Subscription starMissingBackpressureExceptionTest() { - - // Emit one item per millisecond... - return Observable - .interval(1, TimeUnit.MILLISECONDS) - - .doOnNext((number) -> { - Log.v(TAG, "doOnNext() - Emitted number: " + number); - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - - .compose(applySchedulers()) - - // Sleep for 100ms for each emitted item. This will make we receive a BackpressureMissingException quickly. - .subscribe(resultSubscriber(100)); - } - - /** - * By using throttleLast operator (which is pretty much similar to collect) we reduce the chances to get a - * MissingBackpressureException, since it will only emit the last item emitted in a certain period of time. - * - * For this example we are using throttleLast intervalDuration to 100ms, which might be enough to let observer to - * process all emitted items. If we change intervalDuration to a small value (e.g.: 10ms), although we will still - * use throttleLast operator, it will not be enough to prevent buffer's capacity to be full. Try both values to see - * that in action. - * - * @return - */ - private Subscription startThrottleOperatorTest() { - - return Observable - .interval(1, TimeUnit.MILLISECONDS) - - .doOnNext((number) -> { - Log.v(TAG, "doOnNext() - Emitted number: " + number); - - // For this example we will not print emitted items on the GUI, since it would freeze it. Check the logs to see all emitted items - // final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - // w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - - // Using throttleLast intervalDuration equals to 100ms, we will probably not end up in an exception, since our subscriber will be able to - // process all emitted items accordingly. If we change it to 10ms, it will quickly throw a MissingBackpressureException. - .throttleLast(100, TimeUnit.MILLISECONDS) - - // Just for log purpose - .compose(showDebugMessages("throttleLast(100)")) - - // Just adding some boundaries here - .take(20) - - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(100)); - } - - @NonNull - private Subscriber resultSubscriber(final Integer timeToSleep) { - return new Subscriber() { - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " - onCompleted")); - } - - @Override - public void onError(final Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e); - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError" + e)); - } - - @Override - public void onNext(final Long number) { - Log.v(TAG, "subscribe.onNext" + " " + number); - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " " + number)); - - try { - Thread.sleep(timeToSleep); - } catch (InterruptedException e) { - Log.v(TAG, "subscribe.onNext. We got a InterruptedException!"); - } - } - }; - } + } + }; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureManualRequestExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureManualRequestExampleActivity.java index a494da4..af26d7a 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureManualRequestExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureManualRequestExampleActivity.java @@ -7,13 +7,11 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.BaseActivity; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.BaseActivity; import rx.Observable; import rx.Scheduler; import rx.Subscriber; @@ -25,210 +23,201 @@ /** * This example was based on the following article: * https://github.com/Froussios/Intro-To-RxJava/blob/master/Part%204%20-%20Concurrency/4.%20Backpressure.md - * */ public class BackpressureManualRequestExampleActivity extends BaseActivity { - private static final String TAG = BackpressureManualRequestExampleActivity.class.getSimpleName(); - - private ControlledPullSubscriber puller; - - @BindView(R.id.tv_emitted_items) TextView tvEmittedItems; - @BindView(R.id.tv_number_of_requested_items) TextView tvNumberOfRequestedItems; - @BindView(R.id.tv_result) TextView tvResult; - @BindView(R.id.et_number_of_items_to_emit) EditText etNumberOfItemsToEmit; - @BindView(R.id.et_number_of_items_to_request) EditText etNumberOfItemsToRequest; + private static final String TAG = BackpressureManualRequestExampleActivity.class.getSimpleName(); + + private ControlledPullSubscriber puller; + + @BindView(R.id.tv_emitted_items) + TextView tvEmittedItems; + @BindView(R.id.tv_number_of_requested_items) + TextView tvNumberOfRequestedItems; + @BindView(R.id.tv_result) + TextView tvResult; + @BindView(R.id.et_number_of_items_to_emit) + EditText etNumberOfItemsToEmit; + @BindView(R.id.et_number_of_items_to_request) + EditText etNumberOfItemsToRequest; + + private Integer numberOfItemsToEmit; + private Integer numberOfItemsToRequest; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_backpressure_manual_request_example); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); + } - private Integer numberOfItemsToEmit; - private Integer numberOfItemsToRequest; + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backpressure_manual_request_example); - ButterKnife.bind(this); + private void resetData() { + tvNumberOfRequestedItems.setText(""); + tvEmittedItems.setText(""); + tvResult.setText(""); + } - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + @OnClick(R.id.btn_init_test) + public void onInitTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onInitTestButtonClick()"); - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - } + resetData(); + readNumberOfItemsToEmit(); - private void resetData() { - tvNumberOfRequestedItems.setText(""); - tvEmittedItems.setText(""); - tvResult.setText(""); + initTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } + } - @OnClick(R.id.btn_init_test) - public void onInitTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onInitTestButtonClick()"); - - resetData(); - readNumberOfItemsToEmit(); + @OnClick(R.id.btn_request_items) + public void onRequestItemsButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onRequestItemsButtonClick()"); - initTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } + readNumberOfItemsToRequest(); + requestItems(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - @OnClick(R.id.btn_request_items) - public void onRequestItemsButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onRequestItemsButtonClick()"); - - readNumberOfItemsToRequest(); - requestItems(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } + } + + private void readNumberOfItemsToEmit() { + numberOfItemsToEmit = Integer.parseInt(etNumberOfItemsToEmit.getText().toString()); + if (numberOfItemsToEmit < 0) { + numberOfItemsToEmit = 0; + etNumberOfItemsToEmit.setText(numberOfItemsToEmit.toString()); + } else if (numberOfItemsToEmit > 100) { + numberOfItemsToEmit = 100; + etNumberOfItemsToEmit.setText(numberOfItemsToEmit.toString()); } - - private void readNumberOfItemsToEmit() { - numberOfItemsToEmit = Integer.parseInt(etNumberOfItemsToEmit.getText().toString()); - if (numberOfItemsToEmit < 0) { - numberOfItemsToEmit = 0; - etNumberOfItemsToEmit.setText(numberOfItemsToEmit.toString()); - } else if (numberOfItemsToEmit > 100) { - numberOfItemsToEmit = 100; - etNumberOfItemsToEmit.setText(numberOfItemsToEmit.toString()); - } + } + + private void readNumberOfItemsToRequest() { + numberOfItemsToRequest = Integer.parseInt(etNumberOfItemsToRequest.getText().toString()); + if (numberOfItemsToRequest < 0) { + numberOfItemsToRequest = 0; + etNumberOfItemsToRequest.setText(numberOfItemsToRequest.toString()); + } else if (numberOfItemsToRequest > 10) { + numberOfItemsToRequest = 10; + etNumberOfItemsToRequest.setText(numberOfItemsToRequest.toString()); } - - private void readNumberOfItemsToRequest() { - numberOfItemsToRequest = Integer.parseInt(etNumberOfItemsToRequest.getText().toString()); - if (numberOfItemsToRequest < 0) { - numberOfItemsToRequest = 0; - etNumberOfItemsToRequest.setText(numberOfItemsToRequest.toString()); - } else if (numberOfItemsToRequest > 10) { - numberOfItemsToRequest = 10; - etNumberOfItemsToRequest.setText(numberOfItemsToRequest.toString()); - } + } + + private Observable emitNumbers() { + Log.v(TAG, "emitNumbers() - numberOfItemsToEmit: " + numberOfItemsToEmit); + + return Observable.range(0, numberOfItemsToEmit).doOnNext((number) -> { + try { + Log.v(TAG, "emitNumbers() - Emitting number: " + number); + Thread.sleep(100); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedItems.setText(tvEmittedItems.getText() + " " + number)); + }); + } + + /** + * When the test is initialized, we need first to instantiate our Subscriber. Then we subscribe an observable to it. + * + * But note that, since our Subscriber (i.e.: puller) was initialized by calling Subscriber::request(0) in the + * onStart method, no item will be emitted until a call to the Subscriber::request(n) is made with a value greater + * than zero. This is what will happen when user clicks on the "Request Items" button. + */ + private void initTest() { + + puller = new ControlledPullSubscriber<>(() -> { + Log.v(TAG, "ControlledPullSubscriber.onCompleted"); + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvResult.setText(tvResult.getText() + " - onCompleted")); + }, (throwable) -> { + Log.v(TAG, "ControlledPullSubscriber.doOnError: " + throwable.getMessage()); + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError")); + }, (integer) -> { + Log.v(TAG, "ControlledPullSubscriber.onNext"); + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvResult.setText(tvResult.getText() + " " + integer)); + }); + + emitNumbers() + + .subscribeOn(Schedulers.computation()) + + // This side effect method will allow us to see each time items are requested to the observable. + .doOnRequest((item) -> Log.v(TAG, "Requested: " + item)) + + // Subscribe our subscriber which will request for items. + .subscribe(puller); + } + + /** + * When user clicks on the "Request Items" button, we just call our subscriber's requestMore method, which will + * call Subscriber::request(n) method. This will make the subscriber to request the observable to emit more N items. + * + * This way we have total control over how many/when items are emitted. + */ + private void requestItems() { + puller.requestMore(numberOfItemsToRequest); + } + + /** + * This is our Subscriber which starts requesting zero items to the observable (when it is subscriber). It also + * exposes a requestMore(n) method which is called whenever user wants to tells observable to emit items. + */ + public class ControlledPullSubscriber extends Subscriber { + + private final Action1 onNextAction; + private final Action1 onErrorAction; + private final Action0 onCompletedAction; + + public ControlledPullSubscriber(Action0 onCompletedAction, Action1 onErrorAction, + Action1 onNextAction) { + + this.onCompletedAction = onCompletedAction; + this.onErrorAction = onErrorAction; + this.onNextAction = onNextAction; } - private Observable emitNumbers() { - Log.v(TAG, "emitNumbers() - numberOfItemsToEmit: " + numberOfItemsToEmit); - - return Observable - .range(0, numberOfItemsToEmit) - .doOnNext((number) -> { - try { - Log.v(TAG, "emitNumbers() - Emitting number: " + number); - Thread.sleep(100); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } + @Override + public void onStart() { + // Since we request no items at the beginning, that means we are telling the Observable to not emit any item unless we request it + // by calling request() method. This is what requestMore() method does. By doing this, we will have complete control over it, since + // we can call requestMore() at any time when we are able to process data. + request(0); + } - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedItems.setText(tvEmittedItems.getText() + " " + number)); - }); + @Override + public void onCompleted() { + onCompletedAction.call(); } - /** - * When the test is initialized, we need first to instantiate our Subscriber. Then we subscribe an observable to it. - * - * But note that, since our Subscriber (i.e.: puller) was initialized by calling Subscriber::request(0) in the - * onStart method, no item will be emitted until a call to the Subscriber::request(n) is made with a value greater - * than zero. This is what will happen when user clicks on the "Request Items" button. - * - */ - private void initTest() { - - puller = new ControlledPullSubscriber<>( - () -> { - Log.v(TAG, "ControlledPullSubscriber.onCompleted"); - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvResult.setText(tvResult.getText() + " - onCompleted")); - }, - (throwable) -> { - Log.v(TAG, "ControlledPullSubscriber.doOnError: " + throwable.getMessage()); - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError")); - }, - (integer) -> { - Log.v(TAG, "ControlledPullSubscriber.onNext"); - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvResult.setText(tvResult.getText() + " " + integer)); - } - ); - - emitNumbers() - - .subscribeOn(Schedulers.computation()) - - // This side effect method will allow us to see each time items are requested to the observable. - .doOnRequest((item) -> Log.v(TAG, "Requested: " + item)) - - // Subscribe our subscriber which will request for items. - .subscribe(puller); + @Override + public void onError(Throwable e) { + onErrorAction.call(e); } - /** - * When user clicks on the "Request Items" button, we just call our subscriber's requestMore method, which will - * call Subscriber::request(n) method. This will make the subscriber to request the observable to emit more N items. - * - * This way we have total control over how many/when items are emitted. - * - */ - private void requestItems() { - puller.requestMore(numberOfItemsToRequest); + @Override + public void onNext(T t) { + onNextAction.call(t); } - /** - * This is our Subscriber which starts requesting zero items to the observable (when it is subscriber). It also - * exposes a requestMore(n) method which is called whenever user wants to tells observable to emit items. - * - * @param - */ - public class ControlledPullSubscriber extends Subscriber { - - private final Action1 onNextAction; - private final Action1 onErrorAction; - private final Action0 onCompletedAction; - - public ControlledPullSubscriber( - Action0 onCompletedAction, - Action1 onErrorAction, - Action1 onNextAction) { - - this.onCompletedAction = onCompletedAction; - this.onErrorAction = onErrorAction; - this.onNextAction = onNextAction; - } - - @Override - public void onStart() { - // Since we request no items at the beginning, that means we are telling the Observable to not emit any item unless we request it - // by calling request() method. This is what requestMore() method does. By doing this, we will have complete control over it, since - // we can call requestMore() at any time when we are able to process data. - request(0); - } - - @Override - public void onCompleted() { - onCompletedAction.call(); - } - - @Override - public void onError(Throwable e) { - onErrorAction.call(e); - } - - @Override - public void onNext(T t) { - onNextAction.call(t); - } - - public void requestMore(int n) { - request(n); - } + public void requestMore(int n) { + request(n); } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureReactivePullExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureReactivePullExampleActivity.java index 2073e31..b3053c2 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureReactivePullExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureReactivePullExampleActivity.java @@ -6,15 +6,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.BaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.BaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscriber; @@ -23,119 +20,115 @@ public class BackpressureReactivePullExampleActivity extends BaseActivity { - private static final String TAG = BackpressureReactivePullExampleActivity.class.getSimpleName(); + private static final String TAG = BackpressureReactivePullExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backpressure_reactive_pull_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_backpressure_reactive_pull_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_subscriber_with_request_method_call_test) + public void onSubscriberWithRequestMethodCallButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onSubscriberWithRequestMethodCallButtonClick()"); + resetData(); + startSubscriberWithRequestMethodCallTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - @OnClick(R.id.btn_subscriber_with_request_method_call_test) - public void onSubscriberWithRequestMethodCallButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onSubscriberWithRequestMethodCallButtonClick()"); - resetData(); - startSubscriberWithRequestMethodCallTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); + } + + private Observable emitNumbers(final Integer numberOfItemsToBeEmitted, + final int timeToSleep) { + Log.v(TAG, "emitNumbers()"); + + return Observable.range(0, numberOfItemsToBeEmitted).doOnNext((number) -> { + try { + Log.v(TAG, "emitNumbers() - Emitting number: " + number); + Thread.sleep(timeToSleep); + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }); + } + + /** + * This example demonstrates how to use Subscriber::request() method. Since our subscriber was initialized by calling + * Subscriber::request(1), that means one item will be requested at a time. Later, in Subscriber's onNext, only after + * it processes an item, another one will be requested. + */ + private Subscription startSubscriberWithRequestMethodCallTest() { + + return Observable.timer(0, TimeUnit.SECONDS).flatMap((num) -> emitNumbers(20, 10)) + + // Since our subscriber will request for item, this is one way on how we can log it. + .doOnRequest((number) -> Log.v(TAG, "Requested " + number)) + + // Subscribe our subscriber which will request for items. + .subscribe(resultSubscriber()); + } + + @NonNull + private Subscriber resultSubscriber() { + return new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + @Override + public void onError(final Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e); + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError" + e)); + } + + @Override + public void onNext(final Integer number) { + Log.v(TAG, "subscribe.onNext" + " " + number); + + try { + // Sleep for a while. We could do whatever we want here prior to request a new item. This is totally + // up to us + Thread.sleep(500); + + // Now, after "processing" the item, request observable to emit another one + request(1); + } catch (InterruptedException e) { + Log.v(TAG, "subscribe.onNext. We got a InterruptedException!"); } - } - - private Observable emitNumbers(final Integer numberOfItemsToBeEmitted, final int timeToSleep) { - Log.v(TAG, "emitNumbers()"); - - return Observable - .range(0, numberOfItemsToBeEmitted) - .doOnNext((number) -> { - try { - Log.v(TAG, "emitNumbers() - Emitting number: " + number); - Thread.sleep(timeToSleep); - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }); - } - /** - * This example demonstrates how to use Subscriber::request() method. Since our subscriber was initialized by calling - * Subscriber::request(1), that means one item will be requested at a time. Later, in Subscriber's onNext, only after - * it processes an item, another one will be requested. - * - * @return - */ - private Subscription startSubscriberWithRequestMethodCallTest() { - - return Observable - .timer(0, TimeUnit.SECONDS) - .flatMap((num) -> emitNumbers(20, 10)) - - // Since our subscriber will request for item, this is one way on how we can log it. - .doOnRequest((number) -> Log.v(TAG, "Requested " + number)) - - // Subscribe our subscriber which will request for items. - .subscribe(resultSubscriber()); - } - - @NonNull - private Subscriber resultSubscriber() { - return new Subscriber() { - - @Override - public void onStart() { - request(1); - } - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " - onCompleted")); - } - - @Override - public void onError(final Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e); - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError" + e)); - } - - @Override - public void onNext(final Integer number) { - Log.v(TAG, "subscribe.onNext" + " " + number); - - try { - // Sleep for a while. We could do whatever we want here prior to request a new item. This is totally - // up to us - Thread.sleep(500); - - // Now, after "processing" the item, request observable to emit another one - request(1); - } catch (InterruptedException e) { - Log.v(TAG, "subscribe.onNext. We got a InterruptedException!"); - } - - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " " + number)); - } - }; - } + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " " + number)); + } + }; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureSpecificOperatorsExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureSpecificOperatorsExampleActivity.java index 20da5ca..3adbd73 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureSpecificOperatorsExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/backpressure/BackpressureSpecificOperatorsExampleActivity.java @@ -6,16 +6,13 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.BackpressureOverflow; import rx.Observable; import rx.Scheduler; @@ -25,291 +22,281 @@ import rx.schedulers.Schedulers; /** - * * This example shows some specific RxJava operators that handle backpressure - * */ public class BackpressureSpecificOperatorsExampleActivity extends BaseActivity { - private static final String TAG = BackpressureSpecificOperatorsExampleActivity.class.getSimpleName(); - private static final int RANDOM_SLEEP_TIME = -1; - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_overrun_numbers) TextView tvOverrunNumbers; - @BindView(R.id.tv_overrun_caption) TextView tvOverrunCaption; - @BindView(R.id.tv_result) TextView tvResult; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backpressure_specific_operators_example); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - resetData(); + private static final String TAG = + BackpressureSpecificOperatorsExampleActivity.class.getSimpleName(); + private static final int RANDOM_SLEEP_TIME = -1; + + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_overrun_numbers) + TextView tvOverrunNumbers; + @BindView(R.id.tv_overrun_caption) + TextView tvOverrunCaption; + @BindView(R.id.tv_result) + TextView tvResult; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_backpressure_specific_operators_example); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + resetData(); + } - tvOverrunNumbers.setText(""); - tvOverrunNumbers.setTag(null); + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); - tvOverrunCaption.setText(""); - } + tvOverrunNumbers.setText(""); + tvOverrunNumbers.setTag(null); - @OnClick(R.id.btn_onbackpressurebuffer_operator_test) - public void onBackpressureBufferOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onBackpressureBufferOperatorButtonClick()"); - resetData(); - startBackpressureBufferOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } - } + tvOverrunCaption.setText(""); + } - @OnClick(R.id.btn_onbackpressurebuffer_operator_with_an_action_test) - public void onBackpressureBufferOperatorWithAnActionButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onBackpressureBufferOperatorWithAnActionButtonClick()"); - resetData(); - tvOverrunCaption.setText("Overrun"); - tvOverrunNumbers.setTag(0); - startBackpressureBufferOperatorWithAnActionTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } + @OnClick(R.id.btn_onbackpressurebuffer_operator_test) + public void onBackpressureBufferOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onBackpressureBufferOperatorButtonClick()"); + resetData(); + startBackpressureBufferOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - @OnClick(R.id.btn_onbackpressurebuffer_operator_with_an_action_and_a_strategy_test) - public void onBackpressureBufferOperatorWithAnActionAndStrategyButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onBackpressureBufferOperatorWithAnActionAndStrategyButtonClick()"); - resetData(); - tvOverrunCaption.setText("Number of discarded items"); - tvOverrunNumbers.setTag(0); - startBackpressureBufferOperatorWithAnActionAndStrategyTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } + } + + @OnClick(R.id.btn_onbackpressurebuffer_operator_with_an_action_test) + public void onBackpressureBufferOperatorWithAnActionButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onBackpressureBufferOperatorWithAnActionButtonClick()"); + resetData(); + tvOverrunCaption.setText("Overrun"); + tvOverrunNumbers.setTag(0); + startBackpressureBufferOperatorWithAnActionTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - @OnClick(R.id.btn_onbackpressuredrop_operator_test) - public void onBackpressureDropOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onBackpressureDropOperatorButtonClick()"); - resetData(); - tvOverrunCaption.setText("Dropped items"); - startBackpressureDropOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); - } + } + + @OnClick(R.id.btn_onbackpressurebuffer_operator_with_an_action_and_a_strategy_test) + public void onBackpressureBufferOperatorWithAnActionAndStrategyButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onBackpressureBufferOperatorWithAnActionAndStrategyButtonClick()"); + resetData(); + tvOverrunCaption.setText("Number of discarded items"); + tvOverrunNumbers.setTag(0); + startBackpressureBufferOperatorWithAnActionAndStrategyTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } - - private Observable emitNumbers(final Integer numberOfItemsToBeEmitted, final int timeToSleep) { - Log.v(TAG, "emitNumbers()"); - - return Observable - .range(0, numberOfItemsToBeEmitted) - .doOnNext((number) -> { - try { - Log.v(TAG, "emitNumbers() - Emitting number: " + number); - Thread.sleep(timeToSleep); - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }); - } - - /** - * onBackpressureBuffer maintains a buffer of all emissions from the source Observable and emits them to downstream Subscribers according - * to the requests they generate. If the buffer overflows, the observable will fail. - * - * @return - */ - private Subscription startBackpressureBufferOperatorTest() { - - return Observable - .timer(0, TimeUnit.SECONDS) - - .flatMap((num) -> emitNumbers(100, 50)) - - // We are requesting observable to emit 100 items, but our BackpressureBuffer will be able to buffer only - // 40 item. Since we are simulating a heavy processing by sleeping for 200ms on each item, when our buffer - // hits 40 items, it will emit a Overflowed exception - .onBackpressureBuffer(40) - - // Just for log purpose - .compose(showDebugMessages("onBackpressureBuffer")) - - .observeOn(Schedulers.newThread()) - - // Finally subscribe it. - .subscribe(resultSubscriber(200)); + } + + @OnClick(R.id.btn_onbackpressuredrop_operator_test) + public void onBackpressureDropOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onBackpressureDropOperatorButtonClick()"); + resetData(); + tvOverrunCaption.setText("Dropped items"); + startBackpressureDropOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_LONG).show(); } + } - private Subscription startBackpressureBufferOperatorWithAnActionTest() { - - return Observable - .timer(0, TimeUnit.SECONDS) + private Observable emitNumbers(final Integer numberOfItemsToBeEmitted, + final int timeToSleep) { + Log.v(TAG, "emitNumbers()"); - .flatMap((num) -> emitNumbers(100, 50)) + return Observable.range(0, numberOfItemsToBeEmitted).doOnNext((number) -> { + try { + Log.v(TAG, "emitNumbers() - Emitting number: " + number); + Thread.sleep(timeToSleep); - // We are requesting observable to emit 100 items, but our BackpressureBuffer will be able to buffer only - // 40 item. Since we are simulating a heavy processing by sleeping for 200ms on each item, when our buffer - // hits 40 items, it will emit a Overflowed exception, BUT since we are using a variant which accepts an action - // we can take some when it happens (actually we are just printing something on the screen). - .onBackpressureBuffer(40, () -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> { - final Integer count = (Integer) tvOverrunNumbers.getTag() + 1; - Log.d(TAG, "Buffer overrun for: " + count + " time(s)"); - tvOverrunNumbers.setTag(count); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }); + } - tvOverrunNumbers.setText(tvOverrunNumbers.getText() + "Ops, buffer overrun!"); - }); - }) + /** + * onBackpressureBuffer maintains a buffer of all emissions from the source Observable and emits them to downstream Subscribers according + * to the requests they generate. If the buffer overflows, the observable will fail. + */ + private Subscription startBackpressureBufferOperatorTest() { + + return Observable.timer(0, TimeUnit.SECONDS) - // Just for log purpose - .compose(showDebugMessages("onBackpressureBuffer")) + .flatMap((num) -> emitNumbers(100, 50)) - .observeOn(Schedulers.newThread()) + // We are requesting observable to emit 100 items, but our BackpressureBuffer will be able to buffer only + // 40 item. Since we are simulating a heavy processing by sleeping for 200ms on each item, when our buffer + // hits 40 items, it will emit a Overflowed exception + .onBackpressureBuffer(40) - // Finally subscribe it. - .subscribe(resultSubscriber(200)); - } + // Just for log purpose + .compose(showDebugMessages("onBackpressureBuffer")) - private Subscription startBackpressureBufferOperatorWithAnActionAndStrategyTest() { + .observeOn(Schedulers.newThread()) - return Observable - .timer(0, TimeUnit.SECONDS) - - .flatMap((num) -> emitNumbers(100, 10)) - - // We are requesting observable to emit 100 items, but our BackpressureBuffer will be able to buffer only - // 40 item. Since we are simulating a hard processing by sleeping for 200ms on each item, when our buffer - // hits 40 items, it will follow the overflow strategy. On our case, since we are using ON_OVERFLOW_DROP_OLDEST, - // it will drop the oldest item whenever it runs into this situation. - .onBackpressureBuffer( - 40, () -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> { - - // Get some info here. They will be used later in onComplete to show the user. - final Integer count = (Integer) tvOverrunNumbers.getTag() + 1; - Log.d(TAG, "Buffer overrun for: " + count + " time(s)"); - tvOverrunNumbers.setTag(count); - }); - }, - BackpressureOverflow.ON_OVERFLOW_DROP_OLDEST) - - // Just for log purpose - .compose(showDebugMessages("onBackpressureBuffer")) - - .observeOn(Schedulers.newThread()) - - // Finally subscribe it. - .subscribe(resultSubscriber(200)); - } - - /** - * From the docs: - * - * onBackpressureDrop drops emissions from the source Observable unless there is a pending request from a downstream - * Subscriber, in which case it will emit enough items to fulfill the request. - * - * We added a random sleep timer while processing onNext in order to simulate a heavy processing. This will make onBackpressureDrop - * operator to drop some items, since there will be some lack of requests from the downstream. - * - * @return - */ - private Subscription startBackpressureDropOperatorTest() { - - return Observable - .timer(0, TimeUnit.SECONDS) - - .flatMap((num) -> emitNumbers(100, 30)) - - // We are requesting observable to emit 100 items, and also are simulating a hard processing by sleeping - // sometime between 10 and 100ms for each item. Since we are using onBackpressureDrop, overflowed items will be dropped. - .onBackpressureDrop((droppedItem) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvOverrunNumbers.setText(tvOverrunNumbers.getText() + " " + droppedItem)); - }) - - // Just for log purpose - .compose(showDebugMessages("onBackpressureDrop")) - - .observeOn(Schedulers.newThread()) - - // Finally subscribe it. - .subscribe(resultSubscriber(RANDOM_SLEEP_TIME)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(200)); + } + + private Subscription startBackpressureBufferOperatorWithAnActionTest() { + + return Observable.timer(0, TimeUnit.SECONDS) + + .flatMap((num) -> emitNumbers(100, 50)) + + // We are requesting observable to emit 100 items, but our BackpressureBuffer will be able to buffer only + // 40 item. Since we are simulating a heavy processing by sleeping for 200ms on each item, when our buffer + // hits 40 items, it will emit a Overflowed exception, BUT since we are using a variant which accepts an action + // we can take some when it happens (actually we are just printing something on the screen). + .onBackpressureBuffer(40, () -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> { + final Integer count = (Integer) tvOverrunNumbers.getTag() + 1; + Log.d(TAG, "Buffer overrun for: " + count + " time(s)"); + tvOverrunNumbers.setTag(count); + + tvOverrunNumbers.setText(tvOverrunNumbers.getText() + "Ops, buffer overrun!"); + }); + }) + + // Just for log purpose + .compose(showDebugMessages("onBackpressureBuffer")) + + .observeOn(Schedulers.newThread()) + + // Finally subscribe it. + .subscribe(resultSubscriber(200)); + } + + private Subscription startBackpressureBufferOperatorWithAnActionAndStrategyTest() { + + return Observable.timer(0, TimeUnit.SECONDS) + + .flatMap((num) -> emitNumbers(100, 10)) + + // We are requesting observable to emit 100 items, but our BackpressureBuffer will be able to buffer only + // 40 item. Since we are simulating a hard processing by sleeping for 200ms on each item, when our buffer + // hits 40 items, it will follow the overflow strategy. On our case, since we are using ON_OVERFLOW_DROP_OLDEST, + // it will drop the oldest item whenever it runs into this situation. + .onBackpressureBuffer(40, () -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> { + + // Get some info here. They will be used later in onComplete to show the user. + final Integer count = (Integer) tvOverrunNumbers.getTag() + 1; + Log.d(TAG, "Buffer overrun for: " + count + " time(s)"); + tvOverrunNumbers.setTag(count); + }); + }, BackpressureOverflow.ON_OVERFLOW_DROP_OLDEST) + + // Just for log purpose + .compose(showDebugMessages("onBackpressureBuffer")) + + .observeOn(Schedulers.newThread()) + + // Finally subscribe it. + .subscribe(resultSubscriber(200)); + } + + /** + * From the docs: + * + * onBackpressureDrop drops emissions from the source Observable unless there is a pending request from a downstream + * Subscriber, in which case it will emit enough items to fulfill the request. + * + * We added a random sleep timer while processing onNext in order to simulate a heavy processing. This will make onBackpressureDrop + * operator to drop some items, since there will be some lack of requests from the downstream. + */ + private Subscription startBackpressureDropOperatorTest() { + + return Observable.timer(0, TimeUnit.SECONDS) + + .flatMap((num) -> emitNumbers(100, 30)) + + // We are requesting observable to emit 100 items, and also are simulating a hard processing by sleeping + // sometime between 10 and 100ms for each item. Since we are using onBackpressureDrop, overflowed items will be dropped. + .onBackpressureDrop((droppedItem) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule( + () -> tvOverrunNumbers.setText(tvOverrunNumbers.getText() + " " + droppedItem)); + }) + + // Just for log purpose + .compose(showDebugMessages("onBackpressureDrop")) + + .observeOn(Schedulers.newThread()) + + // Finally subscribe it. + .subscribe(resultSubscriber(RANDOM_SLEEP_TIME)); + } + + /** + * This method is used by the onBackpressureBuffer and onBackpressureDrop tests. Since onNext method + * sleeps for a while (in order to simulate a processing longer than the emitted items), if we do this in + * the main thread, GUI it might freeze. So, those notifications are being executed in a new worker thread. + * + * But, when we need to update GUI, of course we need to do it in the main thread. + */ + @NonNull + private Subscriber resultSubscriber(final Integer timeToSleep) { + return new Subscriber() { + + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + // Request GUI to update in the main thread, since this notification is being executed in a worker thread. + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> { + tvResult.setText(tvResult.getText() + " - onCompleted"); + + // This is useful only for the startBackpressureBufferOperatorWithAnActionAndStrategyTest test, once it uses ON_OVERFLOW_DROP_OLDEST + // strategy, making onBackpressureBuffer() operator to not finish with error when its buffer is full, but discard the oldest item. + // Each time it discarded an item, we incremented a counter. Now it is time to show the user how many items were discarded. + if (tvOverrunNumbers.getTag() != null && tvOverrunNumbers.getTag() instanceof Integer) { + tvOverrunNumbers.setText(tvOverrunNumbers.getTag().toString()); + } + }); + } + + @Override + public void onError(final Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e); + // Request GUI to update in the main thread, since this notification is being executed in a worker thread. + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError: " + e)); + } + + @Override + public void onNext(final Integer number) { + Log.v(TAG, "subscribe.onNext" + " " + number); + + // Sleep for a while in order to simulate a hard processing + try { + Thread.sleep(timeToSleep == RANDOM_SLEEP_TIME ? (new Random().nextInt(100 - 10) + 10) + : timeToSleep); + } catch (InterruptedException e) { + Log.v(TAG, "subscribe.onNext. We got a InterruptedException!"); + } - /** - * This method is used by the onBackpressureBuffer and onBackpressureDrop tests. Since onNext method - * sleeps for a while (in order to simulate a processing longer than the emitted items), if we do this in - * the main thread, GUI it might freeze. So, those notifications are being executed in a new worker thread. - * - * But, when we need to update GUI, of course we need to do it in the main thread. - * - * @param timeToSleep - * @return - */ - @NonNull - private Subscriber resultSubscriber(final Integer timeToSleep) { - return new Subscriber() { - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - // Request GUI to update in the main thread, since this notification is being executed in a worker thread. - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> { - tvResult.setText(tvResult.getText() + " - onCompleted"); - - // This is useful only for the startBackpressureBufferOperatorWithAnActionAndStrategyTest test, once it uses ON_OVERFLOW_DROP_OLDEST - // strategy, making onBackpressureBuffer() operator to not finish with error when its buffer is full, but discard the oldest item. - // Each time it discarded an item, we incremented a counter. Now it is time to show the user how many items were discarded. - if (tvOverrunNumbers.getTag() != null && tvOverrunNumbers.getTag() instanceof Integer) { - tvOverrunNumbers.setText(tvOverrunNumbers.getTag().toString()); - } - }); - } - - @Override - public void onError(final Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e); - // Request GUI to update in the main thread, since this notification is being executed in a worker thread. - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " - doOnError: " + e)); - } - - @Override - public void onNext(final Integer number) { - Log.v(TAG, "subscribe.onNext" + " " + number); - - // Sleep for a while in order to simulate a hard processing - try { - Thread.sleep(timeToSleep == RANDOM_SLEEP_TIME ? (new Random().nextInt(100 - 10) + 10) : timeToSleep); - } catch (InterruptedException e) { - Log.v(TAG, "subscribe.onNext. We got a InterruptedException!"); - } - - // Request GUI to update in the main thread, since this notification is being executed in a worker thread. - final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); - w2.schedule(() -> tvResult.setText(tvResult.getText() + " " + number)); - } - }; - } + // Request GUI to update in the main thread, since this notification is being executed in a worker thread. + final Scheduler.Worker w2 = AndroidSchedulers.mainThread().createWorker(); + w2.schedule(() -> tvResult.setText(tvResult.getText() + " " + number)); + } + }; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/base/BaseActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/base/BaseActivity.java index 7311bb0..5604d5a 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/base/BaseActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/base/BaseActivity.java @@ -7,7 +7,6 @@ import android.util.Log; import android.view.MenuItem; import android.widget.TextView; - import rx.Observable; import rx.Subscriber; import rx.Subscription; @@ -16,91 +15,80 @@ public class BaseActivity extends AppCompatActivity { - private static final String TAG = BaseActivity.class.getSimpleName(); + private static final String TAG = BaseActivity.class.getSimpleName(); - protected Subscription subscription; + protected Subscription subscription; - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - // Respond to the action bar's Up/Home button - case android.R.id.home: - Intent intent = NavUtils.getParentActivityIntent(this); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP); - NavUtils.navigateUpTo(this, intent); - return true; - } - return super.onOptionsItemSelected(item); + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // Respond to the action bar's Up/Home button + case android.R.id.home: + Intent intent = NavUtils.getParentActivityIntent(this); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + NavUtils.navigateUpTo(this, intent); + return true; } + return super.onOptionsItemSelected(item); + } - @NonNull - protected Subscriber resultSubscriber(final TextView view) { - return new Subscriber() { + @NonNull + protected Subscriber resultSubscriber(final TextView view) { + return new Subscriber() { - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - view.setText(view.getText() + " - onCompleted"); - } + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + view.setText(view.getText() + " - onCompleted"); + } - @Override - public void onError(Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); - view.setText(view.getText() + " - doOnError"); - } + @Override + public void onError(Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); + view.setText(view.getText() + " - doOnError"); + } - @Override - public void onNext(T number) { + @Override + public void onNext(T number) { - Log.v(TAG, "subscribe.onNext"); - view.setText(view.getText() + " " + number); - } - }; - } + Log.v(TAG, "subscribe.onNext"); + view.setText(view.getText() + " " + number); + } + }; + } - /** - * Print log messages for some side effects methods (or utility methods). - * - * Note that, when calling this method by using Java 7, we must inform explicitly the type T. Otherwise it will assume the Object type, which - * is not compatible,and we will get an error. But when using Java 8, this is no longer needed, since the compiler will infer the correct type. - * This is called a type witness. - * - * From the docs: - * - * "The Java SE 7 compiler [...] requires a value for the type argument T so it starts with the value Object. Consequently, the invocation - * of Collections.emptyList returns a value of type List, which is incompatible with the method..." - * - * "This is no longer necessary in Java SE 8. The notion of what is a target type has been expanded to include method arguments [...]" - * * http://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html - * - * @param operatorName - * @param - * @return - */ - protected Observable.Transformer showDebugMessages(final String operatorName) { + /** + * Print log messages for some side effects methods (or utility methods). + * + * Note that, when calling this method by using Java 7, we must inform explicitly the type T. Otherwise it will assume the Object type, which + * is not compatible,and we will get an error. But when using Java 8, this is no longer needed, since the compiler will infer the correct type. + * This is called a type witness. + * + * From the docs: + * + * "The Java SE 7 compiler [...] requires a value for the type argument T so it starts with the value Object. Consequently, the invocation + * of Collections.emptyList returns a value of type List, which is incompatible with the method..." + * + * "This is no longer necessary in Java SE 8. The notion of what is a target type has been expanded to include method arguments [...]" + * * http://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html + */ + protected Observable.Transformer showDebugMessages(final String operatorName) { - return (observable) -> observable - .doOnSubscribe(() -> Log.v(TAG, operatorName + ".doOnSubscribe")) - .doOnUnsubscribe(() -> Log.v(TAG, operatorName + ".doOnUnsubscribe")) - .doOnNext((doOnNext) -> Log.v(TAG, operatorName + ".doOnNext. Data: " + doOnNext)) - .doOnCompleted(() -> Log.v(TAG, operatorName + ".doOnCompleted")) - .doOnTerminate(() -> Log.v(TAG, operatorName + ".doOnTerminate")) - .doOnError((throwable) -> Log.v(TAG, operatorName + ".doOnError: " + throwable.getMessage())); - } + return (observable) -> observable.doOnSubscribe( + () -> Log.v(TAG, operatorName + ".doOnSubscribe")) + .doOnUnsubscribe(() -> Log.v(TAG, operatorName + ".doOnUnsubscribe")) + .doOnNext((doOnNext) -> Log.v(TAG, operatorName + ".doOnNext. Data: " + doOnNext)) + .doOnCompleted(() -> Log.v(TAG, operatorName + ".doOnCompleted")) + .doOnTerminate(() -> Log.v(TAG, operatorName + ".doOnTerminate")) + .doOnError( + (throwable) -> Log.v(TAG, operatorName + ".doOnError: " + throwable.getMessage())); + } - /** - * Code downloaded from: http://blog.danlew.net/2015/03/02/dont-break-the-chain/ - * - * @param - * @return - */ - protected Observable.Transformer applySchedulers() { - return new Observable.Transformer() { - @Override - public Observable call(Observable observable) { - return observable.subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()); - } - }; - } + /** + * Code downloaded from: http://blog.danlew.net/2015/03/02/dont-break-the-chain/ + */ + protected Observable.Transformer applySchedulers() { + return observable -> observable.subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/base/HotObservablesBaseActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/base/HotObservablesBaseActivity.java index ab1b226..073e4b3 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/base/HotObservablesBaseActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/base/HotObservablesBaseActivity.java @@ -2,47 +2,46 @@ import android.util.Log; import android.widget.Toast; - import rx.Subscription; import rx.observables.ConnectableObservable; public class HotObservablesBaseActivity extends BaseActivity { - private static final String TAG = HotObservablesBaseActivity.class.getSimpleName(); + private static final String TAG = HotObservablesBaseActivity.class.getSimpleName(); - protected ConnectableObservable connectable; - protected Subscription firstSubscription; - protected Subscription secondSubscription; + protected ConnectableObservable connectable; + protected Subscription firstSubscription; + protected Subscription secondSubscription; - /** - * When unsubscribing a subscriber, that means it will stop receiving emitted items. - * - */ - protected void unsubscribeFirst(boolean showMessage) { - Log.v(TAG, "unsubscribeFirst()"); - if (firstSubscription != null && !firstSubscription.isUnsubscribed()) { - Log.v(TAG, "unsubscribeFirst() - Calling unsubscribe..."); - firstSubscription.unsubscribe(); - } else { - if (showMessage) { - Toast.makeText(getApplicationContext(), "Subscriber not started", Toast.LENGTH_SHORT).show(); - } - } + /** + * When unsubscribing a subscriber, that means it will stop receiving emitted items. + */ + protected void unsubscribeFirst(boolean showMessage) { + Log.v(TAG, "unsubscribeFirst()"); + if (firstSubscription != null && !firstSubscription.isUnsubscribed()) { + Log.v(TAG, "unsubscribeFirst() - Calling unsubscribe..."); + firstSubscription.unsubscribe(); + } else { + if (showMessage) { + Toast.makeText(getApplicationContext(), "Subscriber not started", Toast.LENGTH_SHORT) + .show(); + } } + } - /** - * When unsubscribing a subscriber, that means it will stop receiving emitted items. - * - */ - protected void unsubscribeSecond(boolean showMessage) { - Log.v(TAG, "unsubscribeSecond()"); - if (secondSubscription != null && !secondSubscription.isUnsubscribed()) { - Log.v(TAG, "unsubscribeSecond() - Calling unsubscribe..."); - secondSubscription.unsubscribe(); - } else { - if (showMessage) { - Toast.makeText(getApplicationContext(), "Subscriber not started", Toast.LENGTH_SHORT).show(); - } - } + /** + * When unsubscribing a subscriber, that means it will stop receiving emitted items. + */ + protected void unsubscribeSecond(boolean showMessage) { + Log.v(TAG, "unsubscribeSecond()"); + if (secondSubscription != null && !secondSubscription.isUnsubscribed()) { + Log.v(TAG, "unsubscribeSecond() - Calling unsubscribe..."); + secondSubscription.unsubscribe(); + } else { + if (showMessage) { + Toast.makeText(getApplicationContext(), "Subscriber not started", Toast.LENGTH_SHORT) + .show(); + } } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/BroadcastSystemStatusExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/BroadcastSystemStatusExampleActivity.java index 910e085..702592b 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/BroadcastSystemStatusExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/BroadcastSystemStatusExampleActivity.java @@ -13,17 +13,14 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.ToggleButton; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.cantrowitz.rxbroadcast.RxBroadcast; import com.jakewharton.rxbinding.view.RxView; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; -import butterknife.BindView; -import butterknife.ButterKnife; - /** - * * This example uses RxBroadcast library to demonstrate how to listen for broadcast system messages in a reactive way. * * It shows two examples: 1) listen for network status and 2) battery monitoring. @@ -31,90 +28,89 @@ * Toggle button manipulation was taken from http://android--code.blogspot.com.br/2015/08/android-togglebutton-style.html * * Battery icons taken from http://downloadicons.net/power-icons?page=7 - * */ public class BroadcastSystemStatusExampleActivity extends BaseActivity { - private static final String TAG = BroadcastSystemStatusExampleActivity.class.getSimpleName(); - - private static int TYPE_WIFI = 1; - //private static int TYPE_MOBILE = 2; - //private static int TYPE_NOT_CONNECTED = 0; - - private boolean mIsConnected; - private WifiManager wifiManager; - - @BindView(R.id.tv_network_status) TextView tvNetworkStatus; - @BindView(R.id.btn_enable_disable_wifi) ToggleButton btnEnableDisableWiFi; - - @BindView(R.id.tv_battery_status) TextView tvBatteryStatus; - @BindView(R.id.img_battery_status) ImageView imgBatteryLevel; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_general_broadcast_system_status_example); - ButterKnife.bind(this); + private static final String TAG = BroadcastSystemStatusExampleActivity.class.getSimpleName(); + + private static int TYPE_WIFI = 1; + //private static int TYPE_MOBILE = 2; + //private static int TYPE_NOT_CONNECTED = 0; + + private boolean mIsConnected; + private WifiManager wifiManager; + + @BindView(R.id.tv_network_status) + TextView tvNetworkStatus; + @BindView(R.id.btn_enable_disable_wifi) + ToggleButton btnEnableDisableWiFi; + + @BindView(R.id.tv_battery_status) + TextView tvBatteryStatus; + @BindView(R.id.img_battery_status) + ImageView imgBatteryLevel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_general_broadcast_system_status_example); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); + } - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + wifiManager = (WifiManager) this.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - wifiManager = (WifiManager)this.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + // Create a listener for the WiFi Enable/Disable button by using RxBinding library + RxView.clicks(btnEnableDisableWiFi).subscribe(view -> { - // Create a listener for the WiFi Enable/Disable button by using RxBinding library - RxView.clicks(btnEnableDisableWiFi).subscribe(view -> { + Log.v(TAG, "btnEnableDisableWiFi::onClick() - Current WiFi status is: " + + mIsConnected + + ". Requesting WiFiManager to change it..."); - Log.v(TAG, "btnEnableDisableWiFi::onClick() - Current WiFi status is: " + mIsConnected + ". Requesting WiFiManager to change it..."); + // The default toggle control behavior is to change its state when it is clicked, but we are keeping current state by calling + // ToggleButton::setChecked() method and delegating such change to the network broadcast based on the network status. + // This might be a poor solution, since we can face some quick flip when clicking over it. + btnEnableDisableWiFi.setChecked(mIsConnected); - // The default toggle control behavior is to change its state when it is clicked, but we are keeping current state by calling - // ToggleButton::setChecked() method and delegating such change to the network broadcast based on the network status. - // This might be a poor solution, since we can face some quick flip when clicking over it. - btnEnableDisableWiFi.setChecked(mIsConnected); + // Now asks WiFiManager to change WiFi current status + wifiManager.setWifiEnabled(!mIsConnected); + }); - // Now asks WiFiManager to change WiFi current status - wifiManager.setWifiEnabled(!mIsConnected); - }); + // This is the main part of this example. See how easy is to start a broadcast listener when using RxBroadcast. Just create an + // IntentFilter as we normally do and then call RxBroadcast.fromBroadcast(...) to create an observable. We could also filter + // messages the same way we do when using RxJava. + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + RxBroadcast.fromBroadcast(this, filter).subscribe(intent -> handleNetworkConnectivityChanges()); - // This is the main part of this example. See how easy is to start a broadcast listener when using RxBroadcast. Just create an - // IntentFilter as we normally do and then call RxBroadcast.fromBroadcast(...) to create an observable. We could also filter - // messages the same way we do when using RxJava. - IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - RxBroadcast - .fromBroadcast(this, filter) - .subscribe(intent -> handleNetworkConnectivityChanges()); + IntentFilter filter2 = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + RxBroadcast.fromBroadcast(this, filter2).subscribe(intent -> handleBatteryChange(intent)); + } - IntentFilter filter2 = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - RxBroadcast - .fromBroadcast(this, filter2) - .subscribe(intent -> handleBatteryChange(intent)); - } + /** + * This method handles network connectivity status received by the system broadcast. Basically it checks whether there is a wifi + * connection available or not and shows a text message accordingly. + */ + private void handleNetworkConnectivityChanges() { + Log.v(TAG, "handleNetworkConnectivityChanges() - Begin"); - /** - * This method handles network connectivity status received by the system broadcast. Basically it checks whether there is a wifi - * connection available or not and shows a text message accordingly. - * - */ - private void handleNetworkConnectivityChanges() { - Log.v(TAG, "handleNetworkConnectivityChanges() - Begin"); + String status = "Not connected to the Internet"; - String status = "Not connected to the Internet"; + ConnectivityManager cm = (ConnectivityManager) getApplicationContext().getSystemService( + Context.CONNECTIVITY_SERVICE); - ConnectivityManager cm = (ConnectivityManager) getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + mIsConnected = activeNetwork != null + && activeNetwork.isConnectedOrConnecting() + && activeNetwork.getType() == TYPE_WIFI; - NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - mIsConnected = activeNetwork != null - && activeNetwork.isConnectedOrConnecting() - && activeNetwork.getType() == TYPE_WIFI; - - if (mIsConnected) { - if (activeNetwork.getType() == TYPE_WIFI) { - status = "Detected WIFI is enabled"; - btnEnableDisableWiFi.setChecked(true); - Log.v(TAG, "handleNetworkConnectivityChanges() - mIsConnected is true"); - } + if (mIsConnected) { + if (activeNetwork.getType() == TYPE_WIFI) { + status = "Detected WIFI is enabled"; + btnEnableDisableWiFi.setChecked(true); + Log.v(TAG, "handleNetworkConnectivityChanges() - mIsConnected is true"); + } /*// We are not using it since we are only interesting in the WiFi network, but let this code here for future references if (activeNetwork.getType() == TYPE_MOBILE) { @@ -122,95 +118,90 @@ private void handleNetworkConnectivityChanges() { btnEnableDisableWiFi.setChecked(true); }*/ - } else { - btnEnableDisableWiFi.setChecked(false); - Log.v(TAG, "handleNetworkConnectivityChanges() - mIsConnected is false"); - } - - Log.d(TAG, status); - tvNetworkStatus.setText(status); - - Log.v(TAG, "handleNetworkConnectivityChanges() - End"); + } else { + btnEnableDisableWiFi.setChecked(false); + Log.v(TAG, "handleNetworkConnectivityChanges() - mIsConnected is false"); } - /** - * This method handles battery status changes. It uses some extras from received intent so that it can check whether device is being - * charged, and if so, whether it is connected via USB or an AC power supply. Then it shows the power status in a text view. - * - * We could make this example more interesting by taking some action when the battery is low, such as shows a toast, or send a message, - * etc. It is up to you! - * - * See this link for more details on battery monitoring: - * https://developer.android.com/training/monitoring-device-state/battery-monitoring.html - * - * @param intent - */ - private void handleBatteryChange(Intent intent) { - Bundle bundle = intent.getExtras(); - - if (null == bundle) - return; - - boolean isPresent = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false); - //String technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); - int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); - int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); - //int health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0); - //int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0); - int rawlevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); - int level = 0; - - boolean usbCharge = plugged == BatteryManager.BATTERY_PLUGGED_USB; - boolean acCharge = plugged == BatteryManager.BATTERY_PLUGGED_AC; - - //boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || - // status == BatteryManager.BATTERY_STATUS_FULL; - - if(isPresent) { - if (rawlevel >= 0 && scale > 0) { - level = (rawlevel * 100) / scale; - } - - if (usbCharge) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_charging_usb); - tvBatteryStatus.setText("USB Charging"); - - // Full charge - if (level == 100) { - tvBatteryStatus.setText("USB Charging - Full"); - } else { - tvBatteryStatus.setText("USB Charging - Battery Level: " + level + "%"); - } - - } else if (acCharge) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_charging_ac); - // Full charge - if (level == 100) { - tvBatteryStatus.setText("AC Charging - Full"); - } else { - tvBatteryStatus.setText("AC Charging - Battery Level: " + level + "%"); - } - - } else { // not charging - tvBatteryStatus.setText("Battery Level: " + level + "%"); - - if (level == 100) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_full); - } else if (level >= 70) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_almost_full); - } else if (level >= 50) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_half); - } else if (level >= 30) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_low); - } else if (level >= 15) { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_very_low); - } else { - imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_empty); - } - } + Log.d(TAG, status); + tvNetworkStatus.setText(status); + + Log.v(TAG, "handleNetworkConnectivityChanges() - End"); + } + + /** + * This method handles battery status changes. It uses some extras from received intent so that it can check whether device is being + * charged, and if so, whether it is connected via USB or an AC power supply. Then it shows the power status in a text view. + * + * We could make this example more interesting by taking some action when the battery is low, such as shows a toast, or send a message, + * etc. It is up to you! + * + * See this link for more details on battery monitoring: + * https://developer.android.com/training/monitoring-device-state/battery-monitoring.html + */ + private void handleBatteryChange(Intent intent) { + Bundle bundle = intent.getExtras(); + + if (null == bundle) return; + + boolean isPresent = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false); + //String technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); + int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + //int health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0); + //int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0); + int rawlevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int level = 0; + + boolean usbCharge = plugged == BatteryManager.BATTERY_PLUGGED_USB; + boolean acCharge = plugged == BatteryManager.BATTERY_PLUGGED_AC; + + //boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + // status == BatteryManager.BATTERY_STATUS_FULL; + + if (isPresent) { + if (rawlevel >= 0 && scale > 0) { + level = (rawlevel * 100) / scale; + } + + if (usbCharge) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_charging_usb); + tvBatteryStatus.setText("USB Charging"); + + // Full charge + if (level == 100) { + tvBatteryStatus.setText("USB Charging - Full"); + } else { + tvBatteryStatus.setText("USB Charging - Battery Level: " + level + "%"); + } + } else if (acCharge) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_charging_ac); + // Full charge + if (level == 100) { + tvBatteryStatus.setText("AC Charging - Full"); + } else { + tvBatteryStatus.setText("AC Charging - Battery Level: " + level + "%"); + } + } else { // not charging + tvBatteryStatus.setText("Battery Level: " + level + "%"); + + if (level == 100) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_full); + } else if (level >= 70) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_almost_full); + } else if (level >= 50) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_half); + } else if (level >= 30) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_low); + } else if (level >= 15) { + imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_very_low); } else { - imgBatteryLevel.setBackgroundResource(0); - tvBatteryStatus.setText("Could not detected any battery on this device"); + imgBatteryLevel.setBackgroundResource(R.drawable.battery_status_empty); } + } + } else { + imgBatteryLevel.setBackgroundResource(0); + tvBatteryStatus.setText("Could not detected any battery on this device"); } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/DrawingExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/DrawingExampleActivity.java index f8a1c46..5b5826e 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/DrawingExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/DrawingExampleActivity.java @@ -17,15 +17,13 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.SeekBar; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.jakewharton.rxbinding.view.RxMenuItem; import com.jakewharton.rxbinding.view.RxView; import com.jakewharton.rxbinding.widget.RxSeekBar; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - -import butterknife.BindView; -import butterknife.ButterKnife; import rx.Observable; import rx.subjects.PublishSubject; @@ -35,301 +33,295 @@ /** * Examples on this activity are based on the following article (with some enhancements, such as change brush color and size, etc): * - * - http://choruscode.blogspot.com.br/2014/07/rxjava-for-ui-events-on-android-example.html + * - http://choruscode.blogspot.com.br/2014/07/rxjava-for-ui-events-on-android-example.html * * Please, visit it in order to get more details about it. * * Icons were taken from https://code.tutsplus.com/tutorials/android-sdk-create-a-drawing-app-interface-creation--mobile-19021 - * */ -public class DrawingExampleActivity extends BaseActivity { +public class DrawingExampleActivity extends BaseActivity { + + private MouseDragView mouseDragView; + @BindView(R.id.reactive_simple_paint) + LinearLayout layout; + @BindView(R.id.btn_brush) + ImageButton btnBrush; + @BindView(R.id.btn_eraser) + ImageButton btnEraser; + @BindView(R.id.btn_new) + ImageButton btnNew; + @BindView(R.id.brush_size) + SeekBar brushSize; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_general_drawing_example); + ButterKnife.bind(this); + + // This is our custom view which will listen and react for mouse events + mouseDragView = new MouseDragView(this, null); + layout.addView(mouseDragView, new ActionBar.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT)); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); + } - private MouseDragView mouseDragView; - @BindView(R.id.reactive_simple_paint) LinearLayout layout; - @BindView(R.id.btn_brush) ImageButton btnBrush; - @BindView(R.id.btn_eraser) ImageButton btnEraser; - @BindView(R.id.btn_new) ImageButton btnNew; - @BindView(R.id.brush_size) SeekBar brushSize; + // Using RxBindings we can listen for SeekBar the same wey we do for the other views. Just choose the + // appropriate method (on this case we use 'changes' method) + RxSeekBar.changes(brushSize).subscribe(mouseDragView::setBrushSize); + + // These lines demonstrate RxBindings in action for button clicks. Simple enough! + RxView.clicks(btnBrush).subscribe(view -> mouseDragView.brush()); + RxView.clicks(btnEraser).subscribe(view -> mouseDragView.eraser()); + RxView.clicks(btnNew).subscribe(view -> mouseDragView.newFile()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_reactive_ui_drawing_example, menu); + + MenuItem menuBlue = menu.findItem(R.id.blue); + MenuItem menuYellow = menu.findItem(R.id.yellow); + MenuItem menuRed = menu.findItem(R.id.red); + MenuItem menuGreen = menu.findItem(R.id.green); + MenuItem menuBlack = menu.findItem(R.id.black); + + // By using RxBindings we can listen for menu items events the same way we do for the other views. + RxMenuItem.clicks(menuBlue) + + // We added filter() operator here only to demonstrate we can transform emitted data the same way we do when working with RxJava. + // In this case, it will filter out if color is already set to that one user is trying to change to. We no this is actually not necessary, + // but as said before, this is only for demonstration purpose. + .filter(m -> mouseDragView.getCurrentBrushColor() != Color.BLUE).subscribe(item -> { + mouseDragView.changeColor(Color.BLUE); + menuBlue.setChecked(true); + menuYellow.setChecked(false); + menuRed.setChecked(false); + menuGreen.setChecked(false); + menuBlack.setChecked(false); + }); + + RxMenuItem.clicks(menuYellow) + .filter(m -> mouseDragView.getCurrentBrushColor() != Color.YELLOW) + .subscribe(item -> { + mouseDragView.changeColor(Color.YELLOW); + menuBlue.setChecked(false); + menuYellow.setChecked(true); + menuRed.setChecked(false); + menuGreen.setChecked(false); + menuBlack.setChecked(false); + }); + + RxMenuItem.clicks(menuRed) + .filter(m -> mouseDragView.getCurrentBrushColor() != Color.RED) + .subscribe(item -> { + mouseDragView.changeColor(Color.RED); + menuBlue.setChecked(false); + menuYellow.setChecked(false); + menuRed.setChecked(true); + menuGreen.setChecked(false); + menuBlack.setChecked(false); + }); + + RxMenuItem.clicks(menuGreen) + .filter(m -> mouseDragView.getCurrentBrushColor() != Color.GREEN) + .subscribe(item -> { + mouseDragView.changeColor(Color.GREEN); + menuBlue.setChecked(false); + menuYellow.setChecked(false); + menuRed.setChecked(false); + menuGreen.setChecked(true); + menuBlack.setChecked(false); + }); + + RxMenuItem.clicks(menuBlack) + .filter(m -> mouseDragView.getCurrentBrushColor() != Color.BLACK) + .subscribe(item -> { + mouseDragView.changeColor(Color.BLACK); + menuBlue.setChecked(false); + menuYellow.setChecked(false); + menuRed.setChecked(false); + menuGreen.setChecked(false); + menuBlack.setChecked(true); + }); + + return true; + } + + /** + * How it works: + * + * First we create a PublishSubject in order to be able to, when subscribe to it, receive only those items that are emitted by the source + * Observable(s) subsequent to the time of the subscription. + * + * Next, we create an observable from our PublishSubject which will be used to filter mouseMotion events according to our needs. + * + * Then we create three different observables but using filter() operator so that each one will receive only one specific MouseMotion event. + * + * After that we create a MotionEvent listener which will fire mouse event whenever they happen. Inside its action function we call our subject onNext() + * method which will make every mouseEvent to be emitted by our subject. + * + * And finally we subscribe our observable which is interested in the ACTION_DOWN events and start collect all ACTION_MOVE events until ACTION_UP event + * is received. When it happens, we call our draw method to paint the source to the screen. + * + * See comments below for a better understanding. + */ + public static class MouseDragView extends View { + + public enum Action { + PAINT, ERASER, + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_general_drawing_example); - ButterKnife.bind(this); - - // This is our custom view which will listen and react for mouse events - mouseDragView = new MouseDragView(this, null); - layout.addView(mouseDragView, new ActionBar.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT)); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + // First create a PublishSubject which allow us to subscribe to it multiple times + private final PublishSubject mTouchSubject = PublishSubject.create(); - // Using RxBindings we can listen for SeekBar the same wey we do for the other views. Just choose the - // appropriate method (on this case we use 'changes' method) - RxSeekBar.changes(brushSize).subscribe(mouseDragView::setBrushSize); + // Now, create an observable based on the PublishSubject. It will be used to make some transformations in the emitted items + private final Observable mTouches = mTouchSubject.asObservable(); - // These lines demonstrate RxBindings in action for button clicks. Simple enough! - RxView.clicks(btnBrush).subscribe(view -> mouseDragView.brush()); - RxView.clicks(btnEraser).subscribe(view -> mouseDragView.eraser()); - RxView.clicks(btnNew).subscribe(view -> mouseDragView.newFile()); - } + // Here we create three different observables which will be used to emit different types of mouse events. Note that, for each + // one, we are filtering only an specific MotionEvent event type. + private final Observable mDownObservable = + mTouches.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_DOWN); + private final Observable mUpObservable = + mTouches.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_UP); + private final Observable mMovesObservable = + mTouches.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_MOVE); - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_reactive_ui_drawing_example, menu); - - MenuItem menuBlue = menu.findItem(R.id.blue); - MenuItem menuYellow = menu.findItem(R.id.yellow); - MenuItem menuRed = menu.findItem(R.id.red); - MenuItem menuGreen = menu.findItem(R.id.green); - MenuItem menuBlack = menu.findItem(R.id.black); - - // By using RxBindings we can listen for menu items events the same way we do for the other views. - RxMenuItem - .clicks(menuBlue) - - // We added filter() operator here only to demonstrate we can transform emitted data the same way we do when working with RxJava. - // In this case, it will filter out if color is already set to that one user is trying to change to. We no this is actually not necessary, - // but as said before, this is only for demonstration purpose. - .filter(m -> mouseDragView.getCurrentBrushColor() != Color.BLUE) - .subscribe(item -> { - mouseDragView.changeColor(Color.BLUE); - menuBlue.setChecked(true); - menuYellow.setChecked(false); - menuRed.setChecked(false); - menuGreen.setChecked(false); - menuBlack.setChecked(false); - }); - - RxMenuItem - .clicks(menuYellow) - .filter(m -> mouseDragView.getCurrentBrushColor() != Color.YELLOW) - .subscribe(item -> { - mouseDragView.changeColor(Color.YELLOW); - menuBlue.setChecked(false); - menuYellow.setChecked(true); - menuRed.setChecked(false); - menuGreen.setChecked(false); - menuBlack.setChecked(false); - }); - - RxMenuItem - .clicks(menuRed) - .filter(m -> mouseDragView.getCurrentBrushColor() != Color.RED) - .subscribe(item -> { - mouseDragView.changeColor(Color.RED); - menuBlue.setChecked(false); - menuYellow.setChecked(false); - menuRed.setChecked(true); - menuGreen.setChecked(false); - menuBlack.setChecked(false); - }); - - RxMenuItem - .clicks(menuGreen) - .filter(m -> mouseDragView.getCurrentBrushColor() != Color.GREEN) - .subscribe(item -> { - mouseDragView.changeColor(Color.GREEN); - menuBlue.setChecked(false); - menuYellow.setChecked(false); - menuRed.setChecked(false); - menuGreen.setChecked(true); - menuBlack.setChecked(false); - }); - - RxMenuItem - .clicks(menuBlack) - .filter(m -> mouseDragView.getCurrentBrushColor() != Color.BLACK) - .subscribe(item -> { - mouseDragView.changeColor(Color.BLACK); - menuBlue.setChecked(false); - menuYellow.setChecked(false); - menuRed.setChecked(false); - menuGreen.setChecked(false); - menuBlack.setChecked(true); - }); + private Bitmap mBitmap; + private Canvas mCanvas; + private Paint mPaint; + private MouseDragView.Action action; + private Integer currentColor; + + public MouseDragView(Context context, AttributeSet attr) { + super(context, attr); + + // This action will inform our draw what to do + this.action = PAINT; + this.setBackgroundColor(Color.WHITE); + this.currentColor = Color.BLACK; + + // Create a listener for the motion event. This will make our subject to publish mouse events whenever they happen. + setOnTouchListener((View v, MotionEvent event) -> { + mTouchSubject.onNext(event); return true; + }); + + // Now it is time to subscribe our observables in order to do something when they receive events. This is where all the reactive magic happens. + // Actually we will only subscribe the observable interesting in the ACTION_DOWN events. Whenever it happens, it will create a Path object and + // store the coordinates to it while the ACTION_MOVE events are being received. Once ACTION_UP is received it will stop storing path (due the + // takeUntil() operator) and will draw the path to the canvas. + mDownObservable.subscribe(downEvent -> { + final Path path = new Path(); + path.moveTo(downEvent.getX(), downEvent.getY()); + Log.i(downEvent.toString(), "Touch down"); + + mMovesObservable.takeUntil(mUpObservable.doOnNext(upEvent -> { + draw(path); + path.close(); + Log.i(upEvent.toString(), "Touch up"); + })).subscribe(motionEvent -> { + path.lineTo(motionEvent.getX(), motionEvent.getY()); + draw(path); + Log.i(motionEvent.toString(), "Touch move"); + }); + }); + + init(); } - /** - * How it works: - * - * First we create a PublishSubject in order to be able to, when subscribe to it, receive only those items that are emitted by the source - * Observable(s) subsequent to the time of the subscription. - * - * Next, we create an observable from our PublishSubject which will be used to filter mouseMotion events according to our needs. - * - * Then we create three different observables but using filter() operator so that each one will receive only one specific MouseMotion event. - * - * After that we create a MotionEvent listener which will fire mouse event whenever they happen. Inside its action function we call our subject onNext() - * method which will make every mouseEvent to be emitted by our subject. - * - * And finally we subscribe our observable which is interested in the ACTION_DOWN events and start collect all ACTION_MOVE events until ACTION_UP event - * is received. When it happens, we call our draw method to paint the source to the screen. - * - * See comments below for a better understanding. - * - */ - public static class MouseDragView extends View { - - public enum Action { - PAINT, - ERASER, - } - - // First create a PublishSubject which allow us to subscribe to it multiple times - private final PublishSubject mTouchSubject = PublishSubject.create(); - - // Now, create an observable based on the PublishSubject. It will be used to make some transformations in the emitted items - private final Observable mTouches = mTouchSubject.asObservable(); - - // Here we create three different observables which will be used to emit different types of mouse events. Note that, for each - // one, we are filtering only an specific MotionEvent event type. - private final Observable mDownObservable = mTouches.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_DOWN); - private final Observable mUpObservable = mTouches.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_UP); - private final Observable mMovesObservable = mTouches.filter(ev -> ev.getActionMasked() == MotionEvent.ACTION_MOVE); - - private Bitmap mBitmap; - private Canvas mCanvas; - private Paint mPaint; - private MouseDragView.Action action; - private Integer currentColor; - - public MouseDragView(Context context, AttributeSet attr) { - super(context, attr); - - // This action will inform our draw what to do - this.action = PAINT; - - this.setBackgroundColor(Color.WHITE); - this.currentColor = Color.BLACK; - - // Create a listener for the motion event. This will make our subject to publish mouse events whenever they happen. - setOnTouchListener((View v, MotionEvent event) -> { - mTouchSubject.onNext(event); - return true; - }); - - // Now it is time to subscribe our observables in order to do something when they receive events. This is where all the reactive magic happens. - // Actually we will only subscribe the observable interesting in the ACTION_DOWN events. Whenever it happens, it will create a Path object and - // store the coordinates to it while the ACTION_MOVE events are being received. Once ACTION_UP is received it will stop storing path (due the - // takeUntil() operator) and will draw the path to the canvas. - mDownObservable.subscribe(downEvent -> { - final Path path = new Path(); - path.moveTo(downEvent.getX(), downEvent.getY()); - Log.i(downEvent.toString(), "Touch down"); - - mMovesObservable - .takeUntil(mUpObservable - .doOnNext(upEvent -> { - draw(path); - path.close(); - Log.i(upEvent.toString(), "Touch up"); - })) - .subscribe(motionEvent -> { - path.lineTo(motionEvent.getX(), motionEvent.getY()); - draw(path); - Log.i(motionEvent.toString(), "Touch move"); - }); - - }); - - init(); - } + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); + mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); + } - mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - mCanvas = new Canvas(mBitmap); - } + public void init() { + mPaint = new Paint(); + mPaint.setDither(true); + mPaint.setColor(currentColor); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeJoin(Paint.Join.ROUND); + mPaint.setStrokeCap(Paint.Cap.ROUND); + mPaint.setStrokeWidth(3); + } - public void init() { - mPaint = new Paint(); - mPaint.setDither(true); - mPaint.setColor(currentColor); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeJoin(Paint.Join.ROUND); - mPaint.setStrokeCap(Paint.Cap.ROUND); - mPaint.setStrokeWidth(3); + private void draw(Path path) { + if (action == PAINT) { + if (mBitmap == null) { + mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); } - private void draw(Path path) { - if (action == PAINT) { - if (mBitmap == null) { - mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); - mCanvas = new Canvas(mBitmap); - } - - // When painting, use color chosen by the user in the menu options. - mPaint.setColor(currentColor); - mCanvas.drawPath(path, mPaint); - invalidate(); - - // When current action is ERASER, change brush color to white in order to give the eraser effect (since this is the same color as - // the background - } else if (action == ERASER) { - if (mBitmap != null) { - mPaint.setColor(Color.WHITE); - mCanvas.drawPath(path, mPaint); - invalidate(); - } - } + // When painting, use color chosen by the user in the menu options. + mPaint.setColor(currentColor); + mCanvas.drawPath(path, mPaint); + invalidate(); + + // When current action is ERASER, change brush color to white in order to give the eraser effect (since this is the same color as + // the background + } else if (action == ERASER) { + if (mBitmap != null) { + mPaint.setColor(Color.WHITE); + mCanvas.drawPath(path, mPaint); + invalidate(); } + } + } - // This is an event called by the framework when we call invalidate(). - public void onDraw(Canvas canvas) { - if (mBitmap != null) { - canvas.drawBitmap(mBitmap, 0, 0, mPaint); - } - } + // This is an event called by the framework when we call invalidate(). + public void onDraw(Canvas canvas) { + if (mBitmap != null) { + canvas.drawBitmap(mBitmap, 0, 0, mPaint); + } + } - // Used when user change brush side in the seekBar view. - public void setBrushSize(Integer size) { - mPaint.setStrokeWidth(size); - } + // Used when user change brush side in the seekBar view. + public void setBrushSize(Integer size) { + mPaint.setStrokeWidth(size); + } - // Called when user change current color form the menu. - public void changeColor(int newColor) { - currentColor = newColor; - brush(); - } + // Called when user change current color form the menu. + public void changeColor(int newColor) { + currentColor = newColor; + brush(); + } - // Used to avoid change current color the a color that is already the current one. This is not actually needed, but is only - // to demonstrate filter() operator in action. - public Integer getCurrentBrushColor() { - return currentColor; - } + // Used to avoid change current color the a color that is already the current one. This is not actually needed, but is only + // to demonstrate filter() operator in action. + public Integer getCurrentBrushColor() { + return currentColor; + } - // This is called when user chooses to create a new draw - public void newFile() { - if (mBitmap != null) { + // This is called when user chooses to create a new draw + public void newFile() { + if (mBitmap != null) { - mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); - mCanvas = new Canvas(mBitmap); + mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); - final Path path = new Path(); - mCanvas.drawPath(path, mPaint); - invalidate(); + final Path path = new Path(); + mCanvas.drawPath(path, mPaint); + invalidate(); - brush(); - } - } + brush(); + } + } - // Change the current action to ERASER. - public void eraser() { - action = ERASER; - } + // Change the current action to ERASER. + public void eraser() { + action = ERASER; + } - // Change the current action to PAINT. - public void brush() { - action = PAINT; - } + // Change the current action to PAINT. + public void brush() { + action = PAINT; } + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingAfterDataProcessingExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingAfterDataProcessingExampleActivity.java index 144ada1..ac660d9 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingAfterDataProcessingExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingAfterDataProcessingExampleActivity.java @@ -6,15 +6,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.BaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.BaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Observer; import rx.Scheduler; @@ -26,7 +23,7 @@ /** * Examples on this activity were based on this GitHub issue (in the Ben Christensen's answers): - * - https://github.com/ReactiveX/RxJava/issues/448 + * - https://github.com/ReactiveX/RxJava/issues/448 * * * This example differs from the ServerPollingExampleActivity in a way that each time it poll a server, it simulates a local data @@ -35,210 +32,205 @@ * * Note that none of the examples in this activity implements any condition to stop polling (e.g.: takeUntil). So it will emit forever * unless stop button is clicked. - * */ public class ServerPollingAfterDataProcessingExampleActivity extends BaseActivity { - private static final String TAG = ServerPollingAfterDataProcessingExampleActivity.class.getSimpleName(); + private static final String TAG = + ServerPollingAfterDataProcessingExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_server_polling_count) TextView tvServerPollingCount; - @BindView(R.id.tv_local_data_processed) TextView tvLocalDataProcessingCount; + @BindView(R.id.tv_server_polling_count) + TextView tvServerPollingCount; + @BindView(R.id.tv_local_data_processed) + TextView tvLocalDataProcessingCount; - private int pollingAttempt; + private int pollingAttempt; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_general_server_polling_after_data_processing_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_general_server_polling_after_data_processing_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - pollingAttempt = 0; - tvServerPollingCount.setText(""); - tvLocalDataProcessingCount.setText(""); + } + + private void resetData() { + pollingAttempt = 0; + tvServerPollingCount.setText(""); + tvLocalDataProcessingCount.setText(""); + } + + @OnClick(R.id.btn_process_data_recursively) + public void onProcessDataRecursivelyButtonClick() { + Log.v(TAG, "onProcessDataRecursivelyButtonClick()"); + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = processDataRecursively(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_process_data_recursively) - public void onProcessDataRecursivelyButtonClick() { - Log.v(TAG, "onProcessDataRecursivelyButtonClick()"); - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = processDataRecursively(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_process_data_manual_recursion_with_scheduler) + public void onProcessDataManualRecursionWithSchedulerButtonClick() { + Log.v(TAG, "onProcessDataManualRecursionWithSchedulerButtonClick()"); + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = processData_manualRecursion_with_scheduler(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_process_data_manual_recursion_with_scheduler) - public void onProcessDataManualRecursionWithSchedulerButtonClick() { - Log.v(TAG, "onProcessDataManualRecursionWithSchedulerButtonClick()"); - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = processData_manualRecursion_with_scheduler(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_process_data_manual_recursion_with_repeat_when) + public void onProcessDataManualRecursionRepeatWhenButtonClick() { + Log.v(TAG, "onProcessDataManualRecursionRepeatWhenButtonClick()"); + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = processData_manualRecursion_with_repeatWhen(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_process_data_manual_recursion_with_repeat_when) - public void onProcessDataManualRecursionRepeatWhenButtonClick() { - Log.v(TAG, "onProcessDataManualRecursionRepeatWhenButtonClick()"); - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = processData_manualRecursion_with_repeatWhen(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + @OnClick(R.id.btn_stop_data_processing) + public void onStopJobProcessingButtonClick() { + Log.v(TAG, "onStopJobProcessingButtonClick()"); + if (subscription != null) { + subscription.unsubscribe(); } + } - @OnClick(R.id.btn_stop_data_processing) - public void onStopJobProcessingButtonClick() { - Log.v(TAG, "onStopJobProcessingButtonClick()"); - if (subscription != null) { - subscription.unsubscribe(); - } - } + private Observable emitData() { + Log.v(TAG, "emitData()"); - private Observable emitData() { - Log.v(TAG, "emitData()"); + return Observable - return Observable + .just(++pollingAttempt) - .just( ++pollingAttempt) + // This is just to not emit forever and flood our GUI + .take(10) - // This is just to not emit forever and flood our GUI - .take(10) + .doOnNext((number) -> { + try { + Log.v(TAG, "emitItems() - Emitting number: " + number); + Thread.sleep(100); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - .doOnNext((number) -> { - try { - Log.v(TAG, "emitItems() - Emitting number: " + number); - Thread.sleep(100); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule( + () -> tvServerPollingCount.setText(tvServerPollingCount.getText() + " " + number)); + }); + } - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } + /** + * This is a very simple example. It gets a data from the server and, after process it locally, it finishes its job, so + * Subscriber::onCompleted() is called. From there, it calls this method again (recursively). + * + * This will fetch server data forever. We could, of course, add a break condition in order to stop it (e.g.: takeUntil, + * take(N), etc) + */ + private Subscription processDataRecursively() { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvServerPollingCount.setText(tvServerPollingCount.getText() + " " + number)); - }); - } + // Fetch some data from a server (do not forget this is just a simulation. No data is actually being requested to any server). + return emitData() + .map((data) -> { + Log.v(TAG, "flatMap()"); - /** - * This is a very simple example. It gets a data from the server and, after process it locally, it finishes its job, so - * Subscriber::onCompleted() is called. From there, it calls this method again (recursively). - * - * This will fetch server data forever. We could, of course, add a break condition in order to stop it (e.g.: takeUntil, - * take(N), etc) - * - * @return - */ - private Subscription processDataRecursively() { + // Now, after get some data from a server, let's process it. Note this processing takes about 2 seconds to finishes, and + // only after that, it will terminate. + heavyDataProcessing(data); - // Fetch some data from a server (do not forget this is just a simulation. No data is actually being requested to any server). - return emitData() + return data; + }) - .map((data) -> { - Log.v(TAG, "flatMap()"); + .compose(applySchedulers()) - // Now, after get some data from a server, let's process it. Note this processing takes about 2 seconds to finishes, and - // only after that, it will terminate. - heavyDataProcessing(data); + // Finally subscribe it. Note this observer call processDataRecursively() method when it is done. This is the idea for this + // example: when data is finished processed, poll the server again. + .subscribe(new Observer() { - return data; - }) - - .compose(applySchedulers()) - - // Finally subscribe it. Note this observer call processDataRecursively() method when it is done. This is the idea for this - // example: when data is finished processed, poll the server again. - .subscribe(new Observer() { - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted - Calling processDataRecursively() method in order to poll the server again"); - // Once we get here, it means data was processed locally accordingly. So, call processDataRecursively() - // method recursively in order to poll server again. - subscription = processDataRecursively(); - } - - @Override - public void onError(Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); - } - - @Override - public void onNext(Integer data) { - Log.v(TAG, "subscribe.onNext: " + data); - tvLocalDataProcessingCount.setText(tvLocalDataProcessingCount.getText() + " " + data); - } - }); - } + @Override + public void onCompleted() { + Log.v(TAG, + "subscribe.onCompleted - Calling processDataRecursively() method in order to poll the server again"); + // Once we get here, it means data was processed locally accordingly. So, call processDataRecursively() + // method recursively in order to poll server again. + subscription = processDataRecursively(); + } + + @Override + public void onError(Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); + } + + @Override + public void onNext(Integer data) { + Log.v(TAG, "subscribe.onNext: " + data); + tvLocalDataProcessingCount.setText(tvLocalDataProcessingCount.getText() + " " + data); + } + }); + } + + /** + * This method will also poll the server only after finish processing previous data locally, but it uses manual recursion instead. + */ + private Subscription processData_manualRecursion_with_scheduler() { + + return Observable.just("") + + .map((data) -> { + Observable.create((Observable.OnSubscribe) o -> { + final Scheduler.Worker w = Schedulers.newThread().createWorker(); + o.add(w); + + // Create an action that will be scheduled to the Scheduler.Worker + Action0 schedule = new Action0() { + @Override + public void call() { + + // When this action is scheduled, it will poll the server and emit it by calling Subscribe.onNext(), but we need to emit the data itself and not the observable returned + // by the emitData() method. So, in order to get the data, we consume it synchronously (by using toBlocking() and forEach() operators together) and then + // we call Subscriber::onNext() with the data we want to emit + emitData().toBlocking().forEach((data) -> { + // Here we emit data received from the server. It will be consumed by the forEach() operator. Inside that method, data will be processed synchronously, and only after + // that, it we will re-schedule a new server polling. This is exactly what we want to achieve on this example. + o.onNext(data); + }); + + // When we get here, it means data was already processed. So re-schedule this action in order to poll the server again. + w.schedule(this, 10, TimeUnit.MILLISECONDS); + } + }; + + // Schedule our action which will poll the server and emit its data to be processed synchronously, and after the data processing, it will re-schedule itself + // entering in an infinite loop. + w.schedule(schedule); + }) - /** - * This method will also poll the server only after finish processing previous data locally, but it uses manual recursion instead. - * - * @return - */ - private Subscription processData_manualRecursion_with_scheduler() { - - return Observable.just("") - - .map((data) -> { - Observable - .create((Observable.OnSubscribe) o -> { - final Scheduler.Worker w = Schedulers.newThread().createWorker(); - o.add(w); - - // Create an action that will be scheduled to the Scheduler.Worker - Action0 schedule = new Action0() { - @Override - public void call() { - - // When this action is scheduled, it will poll the server and emit it by calling Subscribe.onNext(), but we need to emit the data itself and not the observable returned - // by the emitData() method. So, in order to get the data, we consume it synchronously (by using toBlocking() and forEach() operators together) and then - // we call Subscriber::onNext() with the data we want to emit - emitData() - .toBlocking() - .forEach((data) -> { - // Here we emit data received from the server. It will be consumed by the forEach() operator. Inside that method, data will be processed synchronously, and only after - // that, it we will re-schedule a new server polling. This is exactly what we want to achieve on this example. - o.onNext(data); - }); - - // When we get here, it means data was already processed. So re-schedule this action in order to poll the server again. - w.schedule(this, 10, TimeUnit.MILLISECONDS); - } - }; - - // Schedule our action which will poll the server and emit its data to be processed synchronously, and after the data processing, it will re-schedule itself - // entering in an infinite loop. - w.schedule(schedule); - }) - - - // by using toBlocking() operator, we ensure data will be first processed locally and only then we will re-schedule another server polling. - .toBlocking() - - // this operator will consume data from the server that was emitted by the Subscriber::onNext(). It will process the data synchronously (since we are already in a - // computation thread) and only when it returns a re-schedule will be done. - .forEach((data2) -> { - Log.v(TAG, "forEach() - data: " + data2); - heavyDataProcessing(data2); - - // We get here when data is processed accordingly. So, let's update the GUI. - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvLocalDataProcessingCount.setText(tvLocalDataProcessingCount.getText() + " " + data2)); - - Log.v(TAG, "forEach() - Leaving..."); - }); - - return 0; + // by using toBlocking() operator, we ensure data will be first processed locally and only then we will re-schedule another server polling. + .toBlocking() + + // this operator will consume data from the server that was emitted by the Subscriber::onNext(). It will process the data synchronously (since we are already in a + // computation thread) and only when it returns a re-schedule will be done. + .forEach((data2) -> { + Log.v(TAG, "forEach() - data: " + data2); + heavyDataProcessing(data2); + + // We get here when data is processed accordingly. So, let's update the GUI. + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvLocalDataProcessingCount.setText( + tvLocalDataProcessingCount.getText() + " " + data2)); + + Log.v(TAG, "forEach() - Leaving..."); + }); + + return 0; }) // Just for log purpose @@ -250,99 +242,93 @@ public void call() { // For the test perspective we could avoid subscribing our observable, since all the important job is being done inside the forEach operator. But // we will subscribe it in order to be able to stop it when we want (we have a stop button in the GUI) .subscribe(resultSubscriber()); + } - } - - /** - * This example also uses manual recursion, but using another approach. - * - * @return - */ - private Subscription processData_manualRecursion_with_repeatWhen() { - - return Observable.just("") + /** + * This example also uses manual recursion, but using another approach. + */ + private Subscription processData_manualRecursion_with_repeatWhen() { - .subscribeOn(Schedulers.newThread()) + return Observable.just("") - .map((string) -> { - Log.v(TAG, "map()"); + .subscribeOn(Schedulers.newThread()) - Observable - .timer(0, TimeUnit.SECONDS) - .flatMap((aLong) -> { - Log.v(TAG, "timer() - Calling emitData()..."); - return emitData(); - }) + .map((string) -> { + Log.v(TAG, "map()"); - .repeatWhen((observable) -> { - Log.v(TAG, "repeatWhen()"); - return observable.delay(10, TimeUnit.MILLISECONDS); - }) + Observable.timer(0, TimeUnit.SECONDS).flatMap((aLong) -> { + Log.v(TAG, "timer() - Calling emitData()..."); + return emitData(); + }) - .subscribeOn(Schedulers.computation()) + .repeatWhen((observable) -> { + Log.v(TAG, "repeatWhen()"); + return observable.delay(10, TimeUnit.MILLISECONDS); + }) - .toBlocking() + .subscribeOn(Schedulers.computation()) - .forEach((data) -> { - Log.v(TAG, "forEach() - data: " + data); - heavyDataProcessing(data); + .toBlocking() - // We get here when data is processed accordingly. So, let's update the GUI. - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvLocalDataProcessingCount.setText(tvLocalDataProcessingCount.getText() + " " + data)); - }); - - return ""; - }) + .forEach((data) -> { + Log.v(TAG, "forEach() - data: " + data); + heavyDataProcessing(data); - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // We get here when data is processed accordingly. So, let's update the GUI. + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvLocalDataProcessingCount.setText( + tvLocalDataProcessingCount.getText() + " " + data)); + }); - // For the test perspective we could avoid subscribe our observable, since all the important job is being done inside the forEach operator. But - // we will subscribe it in order to be able to stop it when we want (we have a stop button in the GUI) - .subscribe(resultSubscriber()); - } + return ""; + }) - /** - * This method simulates an intensive data processing. - * - * Actually it will only sleep for a while. This is a block operation, so be sure to call it always in a thread other than the mainThread, - * otherwise you will may end up an ANR (Application Not Responding) error message. - * - * @param data - */ - private void heavyDataProcessing(Integer data) { - - Log.v(TAG, "heavyDataProcessing() - Starting processing of data: " + data); - - try { - Thread.sleep(2000); - Log.v(TAG, "heavyDataProcessing() - Data: " + data + " was processed."); - - } catch (InterruptedException e) { - // If we cancel subscription while job is being processed, we will hit here. - Log.e(TAG, "heavyDataProcessing() - Data: " + data + " was not processed due an interruption."); - } - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - @NonNull - protected Subscriber resultSubscriber() { - return new Subscriber() { - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - } - - @Override - public void onError(Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); - } - - @Override - public void onNext(Integer number) { - Log.v(TAG, "subscribe.onNext"); - } - }; + // For the test perspective we could avoid subscribe our observable, since all the important job is being done inside the forEach operator. But + // we will subscribe it in order to be able to stop it when we want (we have a stop button in the GUI) + .subscribe(resultSubscriber()); + } + + /** + * This method simulates an intensive data processing. + * + * Actually it will only sleep for a while. This is a block operation, so be sure to call it always in a thread other than the mainThread, + * otherwise you will may end up an ANR (Application Not Responding) error message. + */ + private void heavyDataProcessing(Integer data) { + + Log.v(TAG, "heavyDataProcessing() - Starting processing of data: " + data); + + try { + Thread.sleep(2000); + Log.v(TAG, "heavyDataProcessing() - Data: " + data + " was processed."); + } catch (InterruptedException e) { + // If we cancel subscription while job is being processed, we will hit here. + Log.e(TAG, + "heavyDataProcessing() - Data: " + data + " was not processed due an interruption."); } + } + + @NonNull + protected Subscriber resultSubscriber() { + return new Subscriber() { + + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + } + + @Override + public void onError(Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); + } + + @Override + public void onNext(Integer number) { + Log.v(TAG, "subscribe.onNext"); + } + }; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingExampleActivity.java index 35c27b0..9d2e5c1 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/ServerPollingExampleActivity.java @@ -8,19 +8,16 @@ import android.util.Log; import android.util.Pair; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.adapter.SimpleStringAdapter; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Subscriber; import rx.Subscription; @@ -34,454 +31,444 @@ * Actually we are not polling any real server, but using an observable to simulate this situation. * * It was based on the following article: https://medium.com/@v.danylo/server-polling-and-retrying-failed-operations-with-retrofit-and-rxjava-8bcc7e641a5a - * */ public class ServerPollingExampleActivity extends BaseActivity { - /** - * Just a simple class that simulates data retrieved from a server containing a parameter that informs whether it should be considered done or not. - * - */ - public class FakeJob { - private String value; - private boolean jobDone = false; - - public FakeJob(String value) { - this(value, false); - } - - public FakeJob(String value, boolean jobDone) { - this.value = value; - this.jobDone = jobDone; - } - - public boolean isJobDone() { - return jobDone; - } - - @Override - public String toString() { - return value; - } - } - - private static final String TAG = ServerPollingExampleActivity.class.getSimpleName(); - - private static final int COUNTER_START = 1; - private static final int MAX_ATTEMPTS = 3; - - @BindView(R.id.my_list) RecyclerView mListView; - - private SimpleStringAdapter mSimpleStringAdapter; - private Integer pollingAttempt; - - private List> retryMatrix; - - // This is a static list of FakeJob objects. It is used to simulate data polling from the server. Items on this list will be - // emitted sequentially, making only the first 5 items to actually be emitted. The reason is that once the fifth item is emitted, - // since it represents a job done, this will make our "client" to stop polling the server. Items from #6 to 10# should never - // be emitted. - private List fakeJobList = Arrays.asList( - new FakeJob("1"), // Will be always emitted (unless user clicks over stop subscription button) - new FakeJob("2"), // Will be always emitted (unless user clicks over stop subscription button) - new FakeJob("3"), // Will be always emitted (unless user clicks over stop subscription button) - new FakeJob("4"), // Will be always emitted (unless user clicks over stop subscription button) - new FakeJob("5", true), // Should stop emission - new FakeJob("6"), // Should never be emitted - new FakeJob("7"), // Should never be emitted - new FakeJob("8"), // Should never be emitted - new FakeJob("9", true)); // Should never be emitted - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_general_server_polling_example); - ButterKnife.bind(this); - - mListView.setLayoutManager(new LinearLayoutManager(this)); - mSimpleStringAdapter = new SimpleStringAdapter(this); - mListView.setAdapter(mSimpleStringAdapter); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - // This is the retry matrix used in a variant of exponential backoff - retryMatrix = Arrays.asList( - new Pair<>( 1, 1), // First four attempts, sleep 1 second before retry - new Pair<>( 5, 2), // For attempt 5 to 9, sleep 2 second before retry - new Pair<>( 10, 3), // For attempt 10 to 19, sleep 3 second before retry - new Pair<>( 20, 4), // For attempt 20 to 39, sleep 4 second before retry - new Pair<>( 40, 5), // For attempt 40 to 99, sleep 4 second before retry - new Pair<>(100, 6) // For the 100th attempts and next ones, sleep 6 second before retry - ); - } - - private void resetData() { - mSimpleStringAdapter.clear(); - pollingAttempt = 0; - } + /** + * Just a simple class that simulates data retrieved from a server containing a parameter that informs whether it should be considered done or not. + */ + public class FakeJob { + private String value; + private boolean jobDone = false; - @OnClick(R.id.btn_start_polling_interval) - public void onStartPollingIntervalButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartPollingIntervalButtonClick()"); - resetData(); - subscription = startPolling_Interval(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + public FakeJob(String value) { + this(value, false); } - @OnClick(R.id.btn_start_polling_repeat_when_until_job_done) - public void onStartPollingRepeatWhenButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartPollingRepeatWhenButtonClick()"); - resetData(); - subscription = startPolling_repeatWhen_untilJobDone(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + public FakeJob(String value, boolean jobDone) { + this.value = value; + this.jobDone = jobDone; } - @OnClick(R.id.btn_start_polling_repeat_when_for_three_times) - public void onStartPollingRepeatWhenForThreeTimesButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartPollingRepeatWhenForThreeTimesButtonClick()"); - resetData(); - subscription = startPolling_repeatWhen_forThreeTimes(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + public boolean isJobDone() { + return jobDone; } - @OnClick(R.id.btn_start_polling_repeat_when_using_exponential_backoff) - public void onstartPollingRepeatWhenUsingExponentialBackoffButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onstartPollingRepeatWhenUsingExponentialBackoffButtonClick()"); - resetData(); - subscription = startPolling_repeatWhen_exponentialBackoff(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } - - @OnClick(R.id.btn_stop_polling) - public void onStopPollingButtonClick() { - Log.v(TAG, "onStopPollingButtonClick()"); - if (subscription != null) { - subscription.unsubscribe(); - } + @Override + public String toString() { + return value; } - - private Observable emitJob() { - Log.v(TAG, "emitJob()"); - - return Observable - .timer(0, TimeUnit.SECONDS) - .flatMap((aLong) -> Observable.just(fakeJobList.get(pollingAttempt++))) - - .doOnNext((number) -> { - try { - Log.v(TAG, "emitItems() - attempt: " + pollingAttempt); - Thread.sleep(100); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }); + } + + private static final String TAG = ServerPollingExampleActivity.class.getSimpleName(); + + private static final int COUNTER_START = 1; + private static final int MAX_ATTEMPTS = 3; + + @BindView(R.id.my_list) + RecyclerView mListView; + + private SimpleStringAdapter mSimpleStringAdapter; + private Integer pollingAttempt; + + private List> retryMatrix; + + // This is a static list of FakeJob objects. It is used to simulate data polling from the server. Items on this list will be + // emitted sequentially, making only the first 5 items to actually be emitted. The reason is that once the fifth item is emitted, + // since it represents a job done, this will make our "client" to stop polling the server. Items from #6 to 10# should never + // be emitted. + private List fakeJobList = Arrays.asList(new FakeJob("1"), + // Will be always emitted (unless user clicks over stop subscription button) + new FakeJob("2"), + // Will be always emitted (unless user clicks over stop subscription button) + new FakeJob("3"), + // Will be always emitted (unless user clicks over stop subscription button) + new FakeJob("4"), + // Will be always emitted (unless user clicks over stop subscription button) + new FakeJob("5", true), // Should stop emission + new FakeJob("6"), // Should never be emitted + new FakeJob("7"), // Should never be emitted + new FakeJob("8"), // Should never be emitted + new FakeJob("9", true)); // Should never be emitted + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_general_server_polling_example); + ButterKnife.bind(this); + + mListView.setLayoutManager(new LinearLayoutManager(this)); + mSimpleStringAdapter = new SimpleStringAdapter(this); + mListView.setAdapter(mSimpleStringAdapter); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - /** - * This method will generate a random FakeJob (note it does not use fakeJobList) - * - * It has 15% of chance to generate a done job and 85% of chance to generate a not done job. - * - * This is used only by startPolling_repeatWhen_exponentialBackoff test. - * - * @return - */ - private Observable emitRandomJob() { - Log.v(TAG, "emitRandomJob()"); - - return Observable - .timer(0, TimeUnit.SECONDS) - .flatMap((aLong) -> { - FakeJob job; - final Integer randomNumber = new Random().nextInt(100); - pollingAttempt++; - - // When using random job, we have 15% of change to create a done job... - if (randomNumber <= 15) { - job = new FakeJob("Random Job", true); - } else { - // ... and 85% of chance to create a non done job - job = new FakeJob("Random Job"); - } - - return Observable.just(job); - }) - - .doOnNext((number) ->Log.v(TAG, "emitItems() - attempt: " + pollingAttempt)); - } - - /** - * This example requests data every 1 second by using interval operator until gets a job that is considered done. - * Then we stop polling. - * - * @return - */ - private Subscription startPolling_Interval() { - - // Emit an observable each 1 second - return Observable.interval(0, 1000, TimeUnit.MILLISECONDS) - - // Poll the server - .flatMap((aLong) -> { - Log.v(TAG, "flatMap - calling emitData() method..."); - return emitJob(); - }) - - // Just for log purpose - .compose(showDebugMessages("flatMap")) - - // Now, for each job, check whether jobDone flag is true. If so, takeUntil will completes making - // interval operator to also completes. - .takeUntil((fakeJob) -> { - if (fakeJob.isJobDone()) { - Log.v(TAG, "takeUntil - FakeJob done"); - } else { - Log.v(TAG, "takeUntil - FakeJob not finished yet"); - } - return fakeJob.isJobDone(); - }) - - // Just for log purpose - .compose(showDebugMessages("takeUntil")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber()); + // This is the retry matrix used in a variant of exponential backoff + retryMatrix = + Arrays.asList(new Pair<>(1, 1), // First four attempts, sleep 1 second before retry + new Pair<>(5, 2), // For attempt 5 to 9, sleep 2 second before retry + new Pair<>(10, 3), // For attempt 10 to 19, sleep 3 second before retry + new Pair<>(20, 4), // For attempt 20 to 39, sleep 4 second before retry + new Pair<>(40, 5), // For attempt 40 to 99, sleep 4 second before retry + new Pair<>(100, 6) // For the 100th attempts and next ones, sleep 6 second before retry + ); + } + + private void resetData() { + mSimpleStringAdapter.clear(); + pollingAttempt = 0; + } + + @OnClick(R.id.btn_start_polling_interval) + public void onStartPollingIntervalButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartPollingIntervalButtonClick()"); + resetData(); + subscription = startPolling_Interval(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - /** - * This example is similar to the "startPolling_Interval", but using repeatWhen operator instead to poll the server until it - * gets a job considered done. - * - * - * @return - */ - private Subscription startPolling_repeatWhen_untilJobDone() { - - return emitJob() - - // Just for log purpose - .compose(showDebugMessages("emitData")) - - .repeatWhen((observable) -> { - Log.v(TAG, "repeatWhen()"); - - // Wait for one second and emit source observable forever. This will be consumed by the takeUntil() observable which will do its - // job until fakeJob is done. - return observable.flatMap((o) -> { - Log.v(TAG, "flatMap()"); - return Observable.timer(1, TimeUnit.SECONDS); - }); - }) - - // Just for log purpose - .compose(showDebugMessages("repeatWhen")) - - .takeUntil((fakeJob) -> { - if (fakeJob.isJobDone()) { - Log.v(TAG, "takeUntil - FakeJob done"); - } else { - Log.v(TAG, "takeUntil - FakeJob not finished yet"); - } - return fakeJob.isJobDone(); - }) - // Just for log purpose - .compose(showDebugMessages("takeUntil")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber()); + } + + @OnClick(R.id.btn_start_polling_repeat_when_until_job_done) + public void onStartPollingRepeatWhenButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartPollingRepeatWhenButtonClick()"); + resetData(); + subscription = startPolling_repeatWhen_untilJobDone(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - /** - * This example polls the server only for three times. Since it uses emitJob() method to get a job - * - * We known in advanced this example will never get a job that is considered donse, since we are using fakeJobList list that - * contains a done job only in its fifth item. - * - * Of course this is just for demonstration purpose when we need to poll a server a fixed number of times, regardless we get an - * expected value or not. - * - * @return - */ - private Subscription startPolling_repeatWhen_forThreeTimes() { - - return emitJob() - - // Just for log purpose - .compose(showDebugMessages("emitData")) - - .repeatWhen((observable) -> { - Log.v(TAG, "repeatWhen()"); - - return observable.zipWith(Observable.range(COUNTER_START, MAX_ATTEMPTS), (aVoid, integer) -> { - Log.v(TAG, "zipWith()"); - return integer; - }) - .flatMap(o -> { - Log.v(TAG, "flatMap()"); - return Observable.timer(1, TimeUnit.SECONDS); - }); - }) - - // Just for log purpose - .compose(showDebugMessages("repeatWhen")) - - .takeUntil(fakeJob -> { - if (fakeJob.isJobDone()) { - Log.v(TAG, "takeUntil - FakeJob done"); - } else { - Log.v(TAG, "takeUntil - FakeJob not finished yet"); - } - return fakeJob.isJobDone(); - }) - // Just for log purpose - .compose(showDebugMessages("takeUntil")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber()); + } + + @OnClick(R.id.btn_start_polling_repeat_when_for_three_times) + public void onStartPollingRepeatWhenForThreeTimesButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartPollingRepeatWhenForThreeTimesButtonClick()"); + resetData(); + subscription = startPolling_repeatWhen_forThreeTimes(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - /** - * This example polls the server until it gets a job done using a variant of an exponential backoff to increase the time it waits before a new - * server request. Once it gets a job considered done, it resets the exponential backoff counter and re-start pooling the server. - * - * emitRandomJob() returns a random job, so we do not know when it will return a job done or not. Actually there are 15% of chance to get a job done - * and 85% of change to get one that is not done. - * - * Note that it will poll the server forever, or until you click on Stop button. - * - * @return - */ - private Subscription startPolling_repeatWhen_exponentialBackoff() { - - // This method will emit one job at a time. - return emitRandomJob() - - // Just for log purpose - .compose(showDebugMessages("emitData")) - - .repeatWhen(observable -> { - Log.v(TAG, "repeatWhen()"); - - // While we get a job that is not finished yet, we will keep polling server but using an exponential backoff. This is done by the - // method getSecondsToSleep() which will return the number of seconds to sleep based on the number of attempts. - return observable.concatMap(o -> { - Integer numOfSeconds = getSecondsToSleep(pollingAttempt); - Log.v(TAG, "flatMap() - Attempt: " + pollingAttempt + " - Waiting for " + numOfSeconds + " second(s) prior next server polling..."); - - // Sleep the number of seconds returned by the getSecondsToSleep() method. - return Observable.timer(numOfSeconds, TimeUnit.SECONDS); - }); - }) - - // Just for log purpose - .compose(showDebugMessages("repeatWhen")) - - .observeOn(AndroidSchedulers.mainThread()) - - .filter(fakeJob -> { - if (fakeJob.isJobDone()) { - Log.v(TAG, "filter() - FakeJob done. Reset retryMatrix counter."); - - // When we get a done job, we will not filtering it, meaning it will be propagate downstream the subscriber. - // Also we will reset attemptCount so that we will keep polling server for the next done job but using - // retryMatrix from its initial values (i.e.: sleeps for 5 seconds in the first minute, 10 sec next four minutes, - // and so on). - pollingAttempt = 1; - - } else { - Log.v(TAG, "filter() - FakeJob not finished yet"); - - // Since we are in a filter() operator, job is not done, no notification is propagated down the chain, and - // recycler view is not updated with such information. So, we add it here. - mSimpleStringAdapter.addString("Attempt: " + pollingAttempt + " - Job not finished. Waiting " + getSecondsToSleep(pollingAttempt) + " sec"); - mListView.scrollToPosition(mSimpleStringAdapter.getItemCount()-1); - } - - return fakeJob.isJobDone(); - }) - - // Just for log purpose - .compose(showDebugMessages("filter")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber()); + } + + @OnClick(R.id.btn_start_polling_repeat_when_using_exponential_backoff) + public void onstartPollingRepeatWhenUsingExponentialBackoffButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onstartPollingRepeatWhenUsingExponentialBackoffButtonClick()"); + resetData(); + subscription = startPolling_repeatWhen_exponentialBackoff(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @NonNull - private Subscriber resultSubscriber() { - return new Subscriber() { - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - } - - @Override - public void onError(Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); - } - - @Override - public void onNext(FakeJob fakeJob) { - - Log.v(TAG, "subscribe.onNext"); - - String msg = "Attempt: " + pollingAttempt; - if (fakeJob.isJobDone()) { - msg = msg + " - Job done"; - } else { - msg = msg + " - Job not finished yet"; - } - mSimpleStringAdapter.addString(msg); - mListView.scrollToPosition(mSimpleStringAdapter.getItemCount()-1); - } - }; + @OnClick(R.id.btn_stop_polling) + public void onStopPollingButtonClick() { + Log.v(TAG, "onStopPollingButtonClick()"); + if (subscription != null) { + subscription.unsubscribe(); } - - /** - * This method returns the number of seconds an observer will sleeps based on the retry count. It extracts this value - * from the retryMatrix - * - * @param attempt - * @return - */ - private Integer getSecondsToSleep(Integer attempt) { - - Integer secondsToSleep = 0; - for( int i = 0; i < retryMatrix.size() && retryMatrix.get(i).first <= attempt; i++ ) { - secondsToSleep = retryMatrix.get(i).second; + } + + private Observable emitJob() { + Log.v(TAG, "emitJob()"); + + return Observable.timer(0, TimeUnit.SECONDS) + .flatMap((aLong) -> Observable.just(fakeJobList.get(pollingAttempt++))) + + .doOnNext((number) -> { + try { + Log.v(TAG, "emitItems() - attempt: " + pollingAttempt); + Thread.sleep(100); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }); + } + + /** + * This method will generate a random FakeJob (note it does not use fakeJobList) + * + * It has 15% of chance to generate a done job and 85% of chance to generate a not done job. + * + * This is used only by startPolling_repeatWhen_exponentialBackoff test. + */ + private Observable emitRandomJob() { + Log.v(TAG, "emitRandomJob()"); + + return Observable.timer(0, TimeUnit.SECONDS).flatMap((aLong) -> { + FakeJob job; + final Integer randomNumber = new Random().nextInt(100); + pollingAttempt++; + + // When using random job, we have 15% of change to create a done job... + if (randomNumber <= 15) { + job = new FakeJob("Random Job", true); + } else { + // ... and 85% of chance to create a non done job + job = new FakeJob("Random Job"); + } + + return Observable.just(job); + }) + + .doOnNext((number) -> Log.v(TAG, "emitItems() - attempt: " + pollingAttempt)); + } + + /** + * This example requests data every 1 second by using interval operator until gets a job that is considered done. + * Then we stop polling. + */ + private Subscription startPolling_Interval() { + + // Emit an observable each 1 second + return Observable.interval(0, 1000, TimeUnit.MILLISECONDS) + + // Poll the server + .flatMap((aLong) -> { + Log.v(TAG, "flatMap - calling emitData() method..."); + return emitJob(); + }) + + // Just for log purpose + .compose(showDebugMessages("flatMap")) + + // Now, for each job, check whether jobDone flag is true. If so, takeUntil will completes making + // interval operator to also completes. + .takeUntil((fakeJob) -> { + if (fakeJob.isJobDone()) { + Log.v(TAG, "takeUntil - FakeJob done"); + } else { + Log.v(TAG, "takeUntil - FakeJob not finished yet"); + } + return fakeJob.isJobDone(); + }) + + // Just for log purpose + .compose(showDebugMessages("takeUntil")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber()); + } + + /** + * This example is similar to the "startPolling_Interval", but using repeatWhen operator instead to poll the server until it + * gets a job considered done. + */ + private Subscription startPolling_repeatWhen_untilJobDone() { + + return emitJob() + + // Just for log purpose + .compose(showDebugMessages("emitData")) + + .repeatWhen((observable) -> { + Log.v(TAG, "repeatWhen()"); + + // Wait for one second and emit source observable forever. This will be consumed by the takeUntil() observable which will do its + // job until fakeJob is done. + return observable.flatMap((o) -> { + Log.v(TAG, "flatMap()"); + return Observable.timer(1, TimeUnit.SECONDS); + }); + }) + + // Just for log purpose + .compose(showDebugMessages("repeatWhen")) + + .takeUntil((fakeJob) -> { + if (fakeJob.isJobDone()) { + Log.v(TAG, "takeUntil - FakeJob done"); + } else { + Log.v(TAG, "takeUntil - FakeJob not finished yet"); + } + return fakeJob.isJobDone(); + }) + // Just for log purpose + .compose(showDebugMessages("takeUntil")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber()); + } + + /** + * This example polls the server only for three times. Since it uses emitJob() method to get a job + * + * We known in advanced this example will never get a job that is considered donse, since we are using fakeJobList list that + * contains a done job only in its fifth item. + * + * Of course this is just for demonstration purpose when we need to poll a server a fixed number of times, regardless we get an + * expected value or not. + */ + private Subscription startPolling_repeatWhen_forThreeTimes() { + + return emitJob() + + // Just for log purpose + .compose(showDebugMessages("emitData")) + + .repeatWhen((observable) -> { + Log.v(TAG, "repeatWhen()"); + + return observable.zipWith(Observable.range(COUNTER_START, MAX_ATTEMPTS), + (aVoid, integer) -> { + Log.v(TAG, "zipWith()"); + return integer; + }).flatMap(o -> { + Log.v(TAG, "flatMap()"); + return Observable.timer(1, TimeUnit.SECONDS); + }); + }) + + // Just for log purpose + .compose(showDebugMessages("repeatWhen")) + + .takeUntil(fakeJob -> { + if (fakeJob.isJobDone()) { + Log.v(TAG, "takeUntil - FakeJob done"); + } else { + Log.v(TAG, "takeUntil - FakeJob not finished yet"); + } + return fakeJob.isJobDone(); + }) + // Just for log purpose + .compose(showDebugMessages("takeUntil")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber()); + } + + /** + * This example polls the server until it gets a job done using a variant of an exponential backoff to increase the time it waits before a new + * server request. Once it gets a job considered done, it resets the exponential backoff counter and re-start pooling the server. + * + * emitRandomJob() returns a random job, so we do not know when it will return a job done or not. Actually there are 15% of chance to get a job done + * and 85% of change to get one that is not done. + * + * Note that it will poll the server forever, or until you click on Stop button. + */ + private Subscription startPolling_repeatWhen_exponentialBackoff() { + + // This method will emit one job at a time. + return emitRandomJob() + + // Just for log purpose + .compose(showDebugMessages("emitData")) + + .repeatWhen(observable -> { + Log.v(TAG, "repeatWhen()"); + + // While we get a job that is not finished yet, we will keep polling server but using an exponential backoff. This is done by the + // method getSecondsToSleep() which will return the number of seconds to sleep based on the number of attempts. + return observable.concatMap(o -> { + Integer numOfSeconds = getSecondsToSleep(pollingAttempt); + Log.v(TAG, "flatMap() - Attempt: " + + pollingAttempt + + " - Waiting for " + + numOfSeconds + + " second(s) prior next server polling..."); + + // Sleep the number of seconds returned by the getSecondsToSleep() method. + return Observable.timer(numOfSeconds, TimeUnit.SECONDS); + }); + }) + + // Just for log purpose + .compose(showDebugMessages("repeatWhen")) + + .observeOn(AndroidSchedulers.mainThread()) + + .filter(fakeJob -> { + if (fakeJob.isJobDone()) { + Log.v(TAG, "filter() - FakeJob done. Reset retryMatrix counter."); + + // When we get a done job, we will not filtering it, meaning it will be propagate downstream the subscriber. + // Also we will reset attemptCount so that we will keep polling server for the next done job but using + // retryMatrix from its initial values (i.e.: sleeps for 5 seconds in the first minute, 10 sec next four minutes, + // and so on). + pollingAttempt = 1; + } else { + Log.v(TAG, "filter() - FakeJob not finished yet"); + + // Since we are in a filter() operator, job is not done, no notification is propagated down the chain, and + // recycler view is not updated with such information. So, we add it here. + mSimpleStringAdapter.addString( + "Attempt: " + pollingAttempt + " - Job not finished. Waiting " + getSecondsToSleep( + pollingAttempt) + " sec"); + mListView.scrollToPosition(mSimpleStringAdapter.getItemCount() - 1); + } + + return fakeJob.isJobDone(); + }) + + // Just for log purpose + .compose(showDebugMessages("filter")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber()); + } + + @NonNull + private Subscriber resultSubscriber() { + return new Subscriber() { + + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + } + + @Override + public void onError(Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); + } + + @Override + public void onNext(FakeJob fakeJob) { + + Log.v(TAG, "subscribe.onNext"); + + String msg = "Attempt: " + pollingAttempt; + if (fakeJob.isJobDone()) { + msg = msg + " - Job done"; + } else { + msg = msg + " - Job not finished yet"; } + mSimpleStringAdapter.addString(msg); + mListView.scrollToPosition(mSimpleStringAdapter.getItemCount() - 1); + } + }; + } + + /** + * This method returns the number of seconds an observer will sleeps based on the retry count. It extracts this value + * from the retryMatrix + */ + private Integer getSecondsToSleep(Integer attempt) { + + Integer secondsToSleep = 0; + for (int i = 0; i < retryMatrix.size() && retryMatrix.get(i).first <= attempt; i++) { + secondsToSleep = retryMatrix.get(i).second; + } - Log.v(TAG, "getSecondsToSleep() - attempt: " + attempt + " - secondsToSleep: " + secondsToSleep); + Log.v(TAG, + "getSecondsToSleep() - attempt: " + attempt + " - secondsToSleep: " + secondsToSleep); - return secondsToSleep; - } + return secondsToSleep; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/TypingIndicatorExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/TypingIndicatorExampleActivity.java index d6c0853..d0a7e4b 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/TypingIndicatorExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/generalexamples/TypingIndicatorExampleActivity.java @@ -8,16 +8,13 @@ import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.TextView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.jakewharton.rxbinding.widget.RxCompoundButton; import com.jakewharton.rxbinding.widget.RxTextView; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; @@ -31,227 +28,217 @@ * requirements. The third option uses a combination of publish-ambWith-timer operators that will work exactly as we expect. * * You can find information about it at http://www.androidahead.com - * */ public class TypingIndicatorExampleActivity extends BaseActivity { - private static final String TAG = TypingIndicatorExampleActivity.class.getSimpleName(); - - @BindView(R.id.etName) EditText etUserName; - @BindView(R.id.et_text) EditText etText; - @BindView(R.id.container_typing_indicator) LinearLayout containerTypingIndicator; - @BindView(R.id.tv_timer) TextView tvTimer; - @BindView(R.id.tv_typing_indicator) TextView tvTypingIndicator; - @BindView(R.id.rb_operator_window) RadioButton rbOperatorWindow; - @BindView(R.id.rb_operator_buffer) RadioButton rbOperatorBuffer; - @BindView(R.id.rb_operator_publish_amb_timer) RadioButton rbOperatorPublishAmbTimer; - - // Used to avoid memory leak. It will hold all subscribers and will unsubscribe all of them in onDestroy() method. - private CompositeSubscription compositeSubscription; - - // Used for the timer TextView - private Subscription timerSubscription; - - // Use to unsubscribe when user change RadioButton option - private Subscription subscription; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_general_typing_indicator); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - compositeSubscription = new CompositeSubscription(); - - // This will setup an observable to enable/disable etText EditText when etUserName EditText contains more than 3 characters - setupUserNameFields(); - - // Setup a listener for all three radio-buttons used on this app - setupRadioButtons(); + private static final String TAG = TypingIndicatorExampleActivity.class.getSimpleName(); + + @BindView(R.id.etName) + EditText etUserName; + @BindView(R.id.et_text) + EditText etText; + @BindView(R.id.container_typing_indicator) + LinearLayout containerTypingIndicator; + @BindView(R.id.tv_timer) + TextView tvTimer; + @BindView(R.id.tv_typing_indicator) + TextView tvTypingIndicator; + @BindView(R.id.rb_operator_window) + RadioButton rbOperatorWindow; + @BindView(R.id.rb_operator_buffer) + RadioButton rbOperatorBuffer; + @BindView(R.id.rb_operator_publish_amb_timer) + RadioButton rbOperatorPublishAmbTimer; + + // Used to avoid memory leak. It will hold all subscribers and will unsubscribe all of them in onDestroy() method. + private CompositeSubscription compositeSubscription; + + // Used for the timer TextView + private Subscription timerSubscription; + + // Use to unsubscribe when user change RadioButton option + private Subscription subscription; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_general_typing_indicator); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - @Override - protected void onDestroy() { - super.onDestroy(); + compositeSubscription = new CompositeSubscription(); - // Warning! After call unsubscribe, this object will be unusable. This is not a problem here, since it is being - // called on onDestroy. - compositeSubscription.unsubscribe(); - } + // This will setup an observable to enable/disable etText EditText when etUserName EditText contains more than 3 characters + setupUserNameFields(); - private void setupUserNameFields() { + // Setup a listener for all three radio-buttons used on this app + setupRadioButtons(); + } - // Only enable EditText etText when userName length is greater than 3 characters. This is just to demonstrate a - // simple RxBinding usage - Subscription subscription1 = RxTextView.textChanges(etUserName) - .doOnNext(s -> Log.d(TAG, "User Name: " + s)) - .map(s -> (s.length() > 3)) - .subscribe(etText::setEnabled); + @Override + protected void onDestroy() { + super.onDestroy(); - // Add this subscription to the composite in order to unsubscribe it when this activity is destroyed. - compositeSubscription.add(subscription1); - } + // Warning! After call unsubscribe, this object will be unusable. This is not a problem here, since it is being + // called on onDestroy. + compositeSubscription.unsubscribe(); + } - private void setupRadioButtons() { - - RxCompoundButton.checkedChanges(rbOperatorWindow) - .subscribe( - checked -> { - if (checked) { - Log.d(TAG, "rbOperatorWindow checked"); - if (subscription != null) { - subscription.unsubscribe(); - compositeSubscription.remove(subscription); - } - // When user chooses window() operator setup an observable in order to use it. - setupTypingIndicatorUsingWindowOperator(); - } - } - ); - - RxCompoundButton.checkedChanges(rbOperatorBuffer) - .subscribe( - checked -> { - if (checked) { - Log.d(TAG, "rbOperatorBuffer checked"); - if (subscription != null) { - subscription.unsubscribe(); - compositeSubscription.remove(subscription); - } - // When user chooses buffer() operator setup an observable in order to use it. - setupTypingIndicatorUsingBufferOperator(); - } - } - ); - - RxCompoundButton.checkedChanges(rbOperatorPublishAmbTimer) - .subscribe( - checked -> { - if (checked) { - Log.d(TAG, "rbOperatorPublishAmbTimer checked"); - if (subscription != null) { - subscription.unsubscribe(); - compositeSubscription.remove(subscription); - } - // When user chooses ambWith() operator setup an observable in order to use it. - setupTypingIndicatorUsingAmbWithOperator(); - } - } - ); - } + private void setupUserNameFields() { - /** - * This example uses window() operator. We are using a variant which accepts a timespan and a count parameters. This means that it will - * emit an observable either when "count" is reached or when timespan expires. - * - * Basically, if it emits an empty observable, it means user did not type anything within 5 seconds. So we will hide typing indicator - * message. When it emits any value, it means user typed something. So, we will show typing indicator message. - * - * The problem is that when user types something, window() operator makes current window to be emitted, but it will only start another - * window either if user types something else (which makes it to start and close a new window and emit it immediately) or after the latest - * timespan expires. - * - * Let's see an example: - * - * At time 0: a new window is started (which is expected to be opened for 5 seconds) - * At time 1: user types letters 'aaa'. Current window is closed and those items are emitted. This will make typing indicator message to - * appear. - * At time 5: another window is opened. - * At time 10: window is closed: Only now typing indicator message is gone, since now window operator emitted an empty observable. - * Here typing indicator message was visible for 9 seconds. - * At time 14: user types letter 'b'. Current window is closed and that item is emitted. This makes typing indicator message to appear. - * At time 15: another window is opened. - * At time 20: window is closed: Only now typing indicator message is gone. Now typing indicator message was visible for 7 seconds. - * - * We can see that behavior mentioned above when user typed some characters at time 1 making current window to be emitted immediately, but - * only at time 5 it started a new window and emitted it only after 5 seconds (i.e.: at time 10). - * - * Since we want to make typing indicator message to be gone EXACTLY after 5 seconds user stop typing, this does not fit our needs. - * - */ - private void setupTypingIndicatorUsingWindowOperator() { - Log.d(TAG, "setupTypingIndicatorUsingWindowOperator()"); - - subscription = RxTextView - .textChanges(etText) - .filter(t -> t.length() > 2) - .window(5, TimeUnit.SECONDS, 1) - .flatMap(t -> t.toList()) - - // This line will add a log entry whenever window operation emits, even when there observable is empty. - // This is helpful during the development phase. - //.compose(showDebugMessages("window")) + // Only enable EditText etText when userName length is greater than 3 characters. This is just to demonstrate a + // simple RxBinding usage + Subscription subscription1 = RxTextView.textChanges(etUserName) + .doOnNext(s -> Log.d(TAG, "User Name: " + s)) + .map(s -> (s.length() > 3)) + .subscribe(etText::setEnabled); - // This operator will prevent emissions downstream until window changes its status (i.e. if it is emitting an empty observable every second, these - // emissions will not be propagated down until user type something and window() emits that value. - //.distinctUntilChanged() + // Add this subscription to the composite in order to unsubscribe it when this activity is destroyed. + compositeSubscription.add(subscription1); + } - // Now, just map our List to a boolean object based on the list length. - .map(data -> data.size() == 0 ? false : true) - - // This will print false in case of an empty observable (i.e. nothing typed within 5 seconds). Otherwise it will print true. - //.compose(showDebugMessages("map (after window)")) - - // Observe on the Main Thread, since we will touch in the GUI objects. - .observeOn(AndroidSchedulers.mainThread()) + private void setupRadioButtons() { - .subscribe( - hasData -> { - Log.d(TAG, "onNext(): " + hasData); - // This is where the magic happens. If observable contains data, we just print our typing indicator. Otherwise we hide it. - if (hasData) { - tvTypingIndicator.setText(etUserName.getText().toString() + " is typing"); - - // Start a timer in order to show to the user for how long he/she is typing - startTimer(); - } else { - tvTypingIndicator.setText(""); - stopTimer(); - } - }, - error -> Log.e(TAG, "onError(): " + error.getMessage()), - () -> Log.d(TAG, "onCompleted") - ); - - compositeSubscription.add(subscription); - } - - /** - * This example uses buffer() operator. We are using a variant which accepts a timespan and a count parameters. This means that it will - * emit an observable either when "count" is reached or when timespan expires. - * - * For the variant we are using, it will always emit something when it's timespan elapses, regardless whether it is fewer than count. This means - * that our buffer() operator will emit each 5 seconds, no matter what happens. Within this period, if user types something, that item will be - * emitted (since count = 1), and when it elapses 5 seconds since its last bundle emission, it will emit an empty list, making our typing indicator - * message to be gone. - * - * Using buffer() operator differs a little than using window(), but still does not fit what we are looking for since if user types something - * right before timespan elapses (e.g.: 500ms before), typing indicator message will be visible for a very short period of time. - * - * Let's see an example: - * - * At time 0: a new timespan is started (which is expected to be opened for 5 seconds) - * At time 1: user types letters 'aaa'. These items are emitted. This will make typing indicator message to appear. - * At time 5: the first timespan elapses, so an empty list is emitted making typing indicator message to be gone. Notice that the indicator - * message was visible for only 4 seconds. - * At time 9: user types letter 'b'. This item is emitted. This will make typing indicator message to appear. - * At time 10: the latest timespan elapsed, so an empty list is emitted making indicator message to be gone. It was visible for only 1 second. - * - * We clearly see that by using buffer operator, the effect is the opposite than when using window() operator. Depends on the period of its timespan - * user types something, typing indicator message will be visible from some milliseconds to up to 5 seconds. - * - */ - private void setupTypingIndicatorUsingBufferOperator() { - Log.d(TAG, "setupTypingIndicatorUsingBufferOperator()"); - - subscription = RxTextView - .textChanges(etText) - .filter(t -> t.length() > 2) - .buffer(5, TimeUnit.SECONDS, 1) + RxCompoundButton.checkedChanges(rbOperatorWindow).subscribe(checked -> { + if (checked) { + Log.d(TAG, "rbOperatorWindow checked"); + if (subscription != null) { + subscription.unsubscribe(); + compositeSubscription.remove(subscription); + } + // When user chooses window() operator setup an observable in order to use it. + setupTypingIndicatorUsingWindowOperator(); + } + }); + + RxCompoundButton.checkedChanges(rbOperatorBuffer).subscribe(checked -> { + if (checked) { + Log.d(TAG, "rbOperatorBuffer checked"); + if (subscription != null) { + subscription.unsubscribe(); + compositeSubscription.remove(subscription); + } + // When user chooses buffer() operator setup an observable in order to use it. + setupTypingIndicatorUsingBufferOperator(); + } + }); + + RxCompoundButton.checkedChanges(rbOperatorPublishAmbTimer).subscribe(checked -> { + if (checked) { + Log.d(TAG, "rbOperatorPublishAmbTimer checked"); + if (subscription != null) { + subscription.unsubscribe(); + compositeSubscription.remove(subscription); + } + // When user chooses ambWith() operator setup an observable in order to use it. + setupTypingIndicatorUsingAmbWithOperator(); + } + }); + } + + /** + * This example uses window() operator. We are using a variant which accepts a timespan and a count parameters. This means that it will + * emit an observable either when "count" is reached or when timespan expires. + * + * Basically, if it emits an empty observable, it means user did not type anything within 5 seconds. So we will hide typing indicator + * message. When it emits any value, it means user typed something. So, we will show typing indicator message. + * + * The problem is that when user types something, window() operator makes current window to be emitted, but it will only start another + * window either if user types something else (which makes it to start and close a new window and emit it immediately) or after the latest + * timespan expires. + * + * Let's see an example: + * + * At time 0: a new window is started (which is expected to be opened for 5 seconds) + * At time 1: user types letters 'aaa'. Current window is closed and those items are emitted. This will make typing indicator message to + * appear. + * At time 5: another window is opened. + * At time 10: window is closed: Only now typing indicator message is gone, since now window operator emitted an empty observable. + * Here typing indicator message was visible for 9 seconds. + * At time 14: user types letter 'b'. Current window is closed and that item is emitted. This makes typing indicator message to appear. + * At time 15: another window is opened. + * At time 20: window is closed: Only now typing indicator message is gone. Now typing indicator message was visible for 7 seconds. + * + * We can see that behavior mentioned above when user typed some characters at time 1 making current window to be emitted immediately, but + * only at time 5 it started a new window and emitted it only after 5 seconds (i.e.: at time 10). + * + * Since we want to make typing indicator message to be gone EXACTLY after 5 seconds user stop typing, this does not fit our needs. + */ + private void setupTypingIndicatorUsingWindowOperator() { + Log.d(TAG, "setupTypingIndicatorUsingWindowOperator()"); + + subscription = RxTextView.textChanges(etText) + .filter(t -> t.length() > 2) + .window(5, TimeUnit.SECONDS, 1) + .flatMap(t -> t.toList()) + + // This line will add a log entry whenever window operation emits, even when there observable is empty. + // This is helpful during the development phase. + //.compose(showDebugMessages("window")) + + // This operator will prevent emissions downstream until window changes its status (i.e. if it is emitting an empty observable every second, these + // emissions will not be propagated down until user type something and window() emits that value. + //.distinctUntilChanged() + + // Now, just map our List to a boolean object based on the list length. + .map(data -> data.size() == 0 ? false : true) + + // This will print false in case of an empty observable (i.e. nothing typed within 5 seconds). Otherwise it will print true. + //.compose(showDebugMessages("map (after window)")) + + // Observe on the Main Thread, since we will touch in the GUI objects. + .observeOn(AndroidSchedulers.mainThread()) + + .subscribe(hasData -> { + Log.d(TAG, "onNext(): " + hasData); + // This is where the magic happens. If observable contains data, we just print our typing indicator. Otherwise we hide it. + if (hasData) { + tvTypingIndicator.setText(etUserName.getText().toString() + " is typing"); + + // Start a timer in order to show to the user for how long he/she is typing + startTimer(); + } else { + tvTypingIndicator.setText(""); + stopTimer(); + } + }, error -> Log.e(TAG, "onError(): " + error.getMessage()), + () -> Log.d(TAG, "onCompleted")); + + compositeSubscription.add(subscription); + } + + /** + * This example uses buffer() operator. We are using a variant which accepts a timespan and a count parameters. This means that it will + * emit an observable either when "count" is reached or when timespan expires. + * + * For the variant we are using, it will always emit something when it's timespan elapses, regardless whether it is fewer than count. This means + * that our buffer() operator will emit each 5 seconds, no matter what happens. Within this period, if user types something, that item will be + * emitted (since count = 1), and when it elapses 5 seconds since its last bundle emission, it will emit an empty list, making our typing indicator + * message to be gone. + * + * Using buffer() operator differs a little than using window(), but still does not fit what we are looking for since if user types something + * right before timespan elapses (e.g.: 500ms before), typing indicator message will be visible for a very short period of time. + * + * Let's see an example: + * + * At time 0: a new timespan is started (which is expected to be opened for 5 seconds) + * At time 1: user types letters 'aaa'. These items are emitted. This will make typing indicator message to appear. + * At time 5: the first timespan elapses, so an empty list is emitted making typing indicator message to be gone. Notice that the indicator + * message was visible for only 4 seconds. + * At time 9: user types letter 'b'. This item is emitted. This will make typing indicator message to appear. + * At time 10: the latest timespan elapsed, so an empty list is emitted making indicator message to be gone. It was visible for only 1 second. + * + * We clearly see that by using buffer operator, the effect is the opposite than when using window() operator. Depends on the period of its timespan + * user types something, typing indicator message will be visible from some milliseconds to up to 5 seconds. + */ + private void setupTypingIndicatorUsingBufferOperator() { + Log.d(TAG, "setupTypingIndicatorUsingBufferOperator()"); + + subscription = + RxTextView.textChanges(etText).filter(t -> t.length() > 2).buffer(5, TimeUnit.SECONDS, 1) // This line will add a log entry whenever buffer operation emits (i.e.: every 5 seconds), even when there observable is empty. // This is helpful during the development phase. @@ -270,117 +257,105 @@ private void setupTypingIndicatorUsingBufferOperator() { // Observe on the Main Thread, since we will touch in the GUI objects. .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - hasData -> { - Log.d(TAG, "onNext(): " + hasData); - // This is where the magic happens. If observable contains data, we just print our typing indicator. Otherwise we hide it. - if (hasData) { - tvTypingIndicator.setText(etUserName.getText().toString() + " is typing"); - - // Start a timer in order to show to the user for how long he/she is typing - startTimer(); - } else { - tvTypingIndicator.setText(""); - stopTimer(); - } - }, - error -> Log.e(TAG, "onError(): " + error.getMessage()), - () -> Log.d(TAG, "onCompleted") - ); - - compositeSubscription.add(subscription); - } - - /** - * This example uses a combination of publish-ambWith-timer operators. It is based in an answer from David Karnok on an SO question (see link below): - * - http://stackoverflow.com/questions/35873244/reactivex-emit-null-or-sentinel-value-after-timeout - * - * According to the docs, amb() operator can have multiple source observables, but will emit all items from ONLY the first of these Observables - * to emit an item or notification. All the others will be discarded. - * - * So, if user types something, it will emit that value and ignore its timer (from the second observable). On the other hand, if the "5 seconds timer" - * expires prior user type something, it will emit a null value. Later we will check for the emitted value, and if it is null, we hide the typing - * indicator message. When it contains any value, we show the typing indicator message. - * - * This will give us a null emitted item exactly after five seconds of idleness. So, finally, this is what we were looking for! - * - */ - private void setupTypingIndicatorUsingAmbWithOperator() { - Log.d(TAG, "setupTypingIndicatorUsingAmbWithOperator()"); - - PublishSubject publishSubject = PublishSubject.create(); - - subscription = publishSubject. - publish(selector -> selector - .take(1) - .ambWith(Observable - .timer(5, TimeUnit.SECONDS) - .map(v -> (Long)null)) - .repeat() - .takeUntil(selector.ignoreElements()) - ) - - // This operator will prevent emissions downstream until publish changes its status (i.e. if it is emitting an empty observable every second, these - // emissions will not be propagated down until user type something and publish() emits that value. - //.distinctUntilChanged() - - // Observe on the Main Thread, since we will touch in the GUI objects. - .observeOn(AndroidSchedulers.mainThread()) - - .subscribe( - hasData -> { - Log.d(TAG, "onNext(): " + hasData); - if (hasData != null) { - // This is where the magic happens. If observable contains data, we just print our typing indicator. Otherwise we hide it. - tvTypingIndicator.setText(etUserName.getText().toString() + " is typing"); - - // Start a timer in order to show to the user for how long he/she is typing - startTimer(); - } else { - tvTypingIndicator.setText(""); - stopTimer(); - } - }, - error -> Log.e(TAG, "onError(): " + error.getMessage()), - () -> Log.d(TAG, "onCompleted") - ); - - RxTextView - .textChanges(etText) - .filter(t -> t.length() > 2) - .timeInterval() - .map(t -> t.getIntervalInMilliseconds()) - .subscribe(publishSubject::onNext); - - compositeSubscription.add(subscription); + .subscribe(hasData -> { + Log.d(TAG, "onNext(): " + hasData); + // This is where the magic happens. If observable contains data, we just print our typing indicator. Otherwise we hide it. + if (hasData) { + tvTypingIndicator.setText(etUserName.getText().toString() + " is typing"); + + // Start a timer in order to show to the user for how long he/she is typing + startTimer(); + } else { + tvTypingIndicator.setText(""); + stopTimer(); + } + }, error -> Log.e(TAG, "onError(): " + error.getMessage()), + () -> Log.d(TAG, "onCompleted")); + + compositeSubscription.add(subscription); + } + + /** + * This example uses a combination of publish-ambWith-timer operators. It is based in an answer from David Karnok on an SO question (see link below): + * - http://stackoverflow.com/questions/35873244/reactivex-emit-null-or-sentinel-value-after-timeout + * + * According to the docs, amb() operator can have multiple source observables, but will emit all items from ONLY the first of these Observables + * to emit an item or notification. All the others will be discarded. + * + * So, if user types something, it will emit that value and ignore its timer (from the second observable). On the other hand, if the "5 seconds timer" + * expires prior user type something, it will emit a null value. Later we will check for the emitted value, and if it is null, we hide the typing + * indicator message. When it contains any value, we show the typing indicator message. + * + * This will give us a null emitted item exactly after five seconds of idleness. So, finally, this is what we were looking for! + */ + private void setupTypingIndicatorUsingAmbWithOperator() { + Log.d(TAG, "setupTypingIndicatorUsingAmbWithOperator()"); + + PublishSubject publishSubject = PublishSubject.create(); + + subscription = publishSubject. + publish(selector -> selector.take(1) + .ambWith(Observable.timer(5, TimeUnit.SECONDS).map(v -> (Long) null)) + .repeat() + .takeUntil(selector.ignoreElements())) + + // This operator will prevent emissions downstream until publish changes its status (i.e. if it is emitting an empty observable every second, these + // emissions will not be propagated down until user type something and publish() emits that value. + //.distinctUntilChanged() + + // Observe on the Main Thread, since we will touch in the GUI objects. + .observeOn(AndroidSchedulers.mainThread()) + + .subscribe(hasData -> { + Log.d(TAG, "onNext(): " + hasData); + if (hasData != null) { + // This is where the magic happens. If observable contains data, we just print our typing indicator. Otherwise we hide it. + tvTypingIndicator.setText(etUserName.getText().toString() + " is typing"); + + // Start a timer in order to show to the user for how long he/she is typing + startTimer(); + } else { + tvTypingIndicator.setText(""); + stopTimer(); + } + }, error -> Log.e(TAG, "onError(): " + error.getMessage()), + () -> Log.d(TAG, "onCompleted")); + + RxTextView.textChanges(etText) + .filter(t -> t.length() > 2) + .timeInterval() + .map(t -> t.getIntervalInMilliseconds()) + .subscribe(publishSubject::onNext); + + compositeSubscription.add(subscription); + } + + private void startTimer() { + Log.d(TAG, "startTimer()"); + + stopTimer(); + + // When starting a timer, it means user is typing something, so make that control visible + containerTypingIndicator.setVisibility(View.VISIBLE); + + // Create a timer which will update a TextView every second in order to show for how long user is typing. + timerSubscription = Observable.interval(0, 1, TimeUnit.SECONDS) + .map((tick) -> tick + 1) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + (tick) -> tvTimer.setText(String.format("%02d:%02d", (tick % 3600) / 60, tick % 60)), + (throwable) -> Log.e(TAG, throwable.getMessage(), throwable)); + } + + private void stopTimer() { + Log.d(TAG, "stopTimer()"); + + // Stop a timer if available + if (timerSubscription != null) { + timerSubscription.unsubscribe(); } - private void startTimer() { - Log.d(TAG, "startTimer()"); - - stopTimer(); - - // When starting a timer, it means user is typing something, so make that control visible - containerTypingIndicator.setVisibility(View.VISIBLE); - - // Create a timer which will update a TextView every second in order to show for how long user is typing. - timerSubscription = Observable.interval(0, 1, TimeUnit.SECONDS) - .map((tick) -> tick+1) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - (tick) -> tvTimer.setText(String.format("%02d:%02d", (tick % 3600) / 60, tick % 60)), - (throwable) -> Log.e(TAG, throwable.getMessage(), throwable)); - } - - private void stopTimer() { - Log.d(TAG, "stopTimer()"); - - // Stop a timer if available - if (timerSubscription != null) { - timerSubscription.unsubscribe(); - } - - // When stopping a timer, it means user stopped typing. So, hide that control. - containerTypingIndicator.setVisibility(View.INVISIBLE); - } + // When stopping a timer, it means user stopped typing. So, hide that control. + containerTypingIndicator.setVisibility(View.INVISIBLE); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableCacheExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableCacheExampleActivity.java index 2b67b97..feb8fa8 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableCacheExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableCacheExampleActivity.java @@ -5,15 +5,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -21,148 +18,149 @@ /** * This example demonstrates how to use ConnectableObservable::cache() operator. - * + * * It ensures that all observers see the same sequence of emitted items, even if they subscribe after the Observable has begun emitting items. - * */ public class HotObservableCacheExampleActivity extends HotObservablesBaseActivity { - private static final String TAG = HotObservableCacheExampleActivity.class.getSimpleName(); + private static final String TAG = HotObservableCacheExampleActivity.class.getSimpleName(); - private Observable observable; + private Observable observable; - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result_first_subscription) TextView tvResultFirstSubscription; - @BindView(R.id.tv_result_second_subscription) TextView tvResultSecondSubscription; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result_first_subscription) + TextView tvResultFirstSubscription; + @BindView(R.id.tv_result_second_subscription) + TextView tvResultSecondSubscription; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_hot_observable_cache_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_hot_observable_cache_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResultFirstSubscription.setText(""); - tvResultSecondSubscription.setText(""); - } - - @OnClick(R.id.btn_cache) - public void onCacheButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onCacheButtonClick()"); - resetData(); - cache(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResultFirstSubscription.setText(""); + tvResultSecondSubscription.setText(""); + } + + @OnClick(R.id.btn_cache) + public void onCacheButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onCacheButtonClick()"); + resetData(); + cache(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_subscribe_first) - public void onSubscribeFirstButtonClick() { + @OnClick(R.id.btn_subscribe_first) + public void onSubscribeFirstButtonClick() { - if (observable == null) { - Log.v(TAG, "onSubscribeFirstButtonClick() - Cannot start a subscriber. You must first call cache()."); + if (observable == null) { + Log.v(TAG, + "onSubscribeFirstButtonClick() - Cannot start a subscriber. You must first call cache()."); - Toast.makeText(getApplicationContext(), "You must first call cache()", Toast.LENGTH_SHORT).show(); - return; - } - - if (firstSubscription == null || (firstSubscription != null && firstSubscription.isUnsubscribed())) { - Log.v(TAG, "onSubscribeFirstButtonClick()"); - firstSubscription = subscribeFirst(); - } else { - Toast.makeText(getApplicationContext(), "First subscriber already started", Toast.LENGTH_SHORT).show(); - } + Toast.makeText(getApplicationContext(), "You must first call cache()", Toast.LENGTH_SHORT) + .show(); + return; } - @OnClick(R.id.btn_subscribe_second) - public void onSubscribeSecondButtonClick() { - - if (observable == null) { - Log.v(TAG, "onSubscribeFirstButtonClick() - Cannot start a subscriber. You must first call cache()."); - - Toast.makeText(getApplicationContext(), "You must first call cache()", Toast.LENGTH_SHORT).show(); - return; - } - if (secondSubscription == null || (secondSubscription != null && secondSubscription.isUnsubscribed())) { - Log.v(TAG, "onSubscribeSecondButtonClick()"); - secondSubscription = subscribeSecond(); - } else { - Toast.makeText(getApplicationContext(), "Second subscriber already started", Toast.LENGTH_SHORT).show(); - } + if (firstSubscription == null || (firstSubscription != null + && firstSubscription.isUnsubscribed())) { + Log.v(TAG, "onSubscribeFirstButtonClick()"); + firstSubscription = subscribeFirst(); + } else { + Toast.makeText(getApplicationContext(), "First subscriber already started", + Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_unsubscribe_first) - public void onUnsubscribeFirstButtonClick() { - Log.v(TAG, "onUnsubscribeFirstButtonClick()"); - unsubscribeFirst(true); - } + @OnClick(R.id.btn_subscribe_second) + public void onSubscribeSecondButtonClick() { - @OnClick(R.id.btn_unsubscribe_second) - public void onUnsubscribeSecondButtonClick() { - Log.v(TAG, "onUnsubscribeSecondButtonClick()"); - unsubscribeSecond(true); - } - - /** - * When calling cache, that will NOT make observable to start emit items. It will only start emitting items when a first - * subscriber subscribes to it. Then, it will receive all cached items. - * - * Just using take(30) in order to prevent it to emit forever. - * - * @return - */ - private void cache() { - Log.v(TAG, "cache()"); - - // cache returns Observable that is connected as long as there are subscribers to it. - observable = Observable - .interval(750, TimeUnit.MILLISECONDS) - .doOnNext((number) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - - // Prevent our observable to emit forever - .take(30) - - .cache(); - } + if (observable == null) { + Log.v(TAG, + "onSubscribeFirstButtonClick() - Cannot start a subscriber. You must first call cache()."); - /** - * If this is the first subscription, it will make observable to start emitting items. But, if there is - * already another subscription, it means that observable has already started emitting items and collecting them. - * So, after we subscribe to it, it will first receive all collected items. - * - * @return - */ - private Subscription subscribeFirst() { - Log.v(TAG, "subscribeFirst()"); - - return observable - .compose(applySchedulers()) - .subscribe(HotObservableCacheExampleActivity.this.resultSubscriber(tvResultFirstSubscription)); + Toast.makeText(getApplicationContext(), "You must first call cache()", Toast.LENGTH_SHORT) + .show(); + return; } - - /** - * If this is the first subscription, it will make observable to start emitting items. But, if there is - * already another subscription, it means that observable has already started emitting items and collecting them. - * So, after we subscribe to it, it will first receive all collected items. - * - * @return - */ - private Subscription subscribeSecond() { - Log.v(TAG, "subscribeSecond()"); - - return observable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultSecondSubscription)); + if (secondSubscription == null || (secondSubscription != null + && secondSubscription.isUnsubscribed())) { + Log.v(TAG, "onSubscribeSecondButtonClick()"); + secondSubscription = subscribeSecond(); + } else { + Toast.makeText(getApplicationContext(), "Second subscriber already started", + Toast.LENGTH_SHORT).show(); } + } + + @OnClick(R.id.btn_unsubscribe_first) + public void onUnsubscribeFirstButtonClick() { + Log.v(TAG, "onUnsubscribeFirstButtonClick()"); + unsubscribeFirst(true); + } + + @OnClick(R.id.btn_unsubscribe_second) + public void onUnsubscribeSecondButtonClick() { + Log.v(TAG, "onUnsubscribeSecondButtonClick()"); + unsubscribeSecond(true); + } + + /** + * When calling cache, that will NOT make observable to start emit items. It will only start emitting items when a first + * subscriber subscribes to it. Then, it will receive all cached items. + * + * Just using take(30) in order to prevent it to emit forever. + */ + private void cache() { + Log.v(TAG, "cache()"); + + // cache returns Observable that is connected as long as there are subscribers to it. + observable = Observable.interval(750, TimeUnit.MILLISECONDS).doOnNext((number) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + // Prevent our observable to emit forever + .take(30) + + .cache(); + } + + /** + * If this is the first subscription, it will make observable to start emitting items. But, if there is + * already another subscription, it means that observable has already started emitting items and collecting them. + * So, after we subscribe to it, it will first receive all collected items. + */ + private Subscription subscribeFirst() { + Log.v(TAG, "subscribeFirst()"); + + return observable.compose(applySchedulers()) + .subscribe( + HotObservableCacheExampleActivity.this.resultSubscriber(tvResultFirstSubscription)); + } + + /** + * If this is the first subscription, it will make observable to start emitting items. But, if there is + * already another subscription, it means that observable has already started emitting items and collecting them. + * So, after we subscribe to it, it will first receive all collected items. + */ + private Subscription subscribeSecond() { + Log.v(TAG, "subscribeSecond()"); + + return observable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultSecondSubscription)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableConnectExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableConnectExampleActivity.java index 564fa50..22a67ae 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableConnectExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableConnectExampleActivity.java @@ -5,15 +5,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -21,157 +18,153 @@ /** * This example demonstrates how to use ConnectableObservable::connect() operator. - * */ public class HotObservableConnectExampleActivity extends HotObservablesBaseActivity { - private static final String TAG = HotObservableConnectExampleActivity.class.getSimpleName(); - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result_first_subscription) TextView tvResultFirstSubscription; - @BindView(R.id.tv_result_second_subscription) TextView tvResultSecondSubscription; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_hot_observable_connect_example); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - // We will create the ConnectableObservable in the onCreate() method, so it will be available for the two - // subscribers to subscribe to it, even before call ConnectionObservable::connect(). - connectable = Observable - .interval(500, TimeUnit.MILLISECONDS) - .doOnNext((number) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - - // This will convert the Observable to a ConnectableObservable - .publish(); - } + private static final String TAG = HotObservableConnectExampleActivity.class.getSimpleName(); - private void resetData() { - tvEmittedNumbers.setText(""); - tvResultFirstSubscription.setText(""); - tvResultSecondSubscription.setText(""); - } + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result_first_subscription) + TextView tvResultFirstSubscription; + @BindView(R.id.tv_result_second_subscription) + TextView tvResultSecondSubscription; - @OnClick(R.id.btn_connect) - public void onConnectButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onConnectButtonClick()"); - resetData(); - subscription = connect(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_hot_observable_connect_example); + ButterKnife.bind(this); - @OnClick(R.id.btn_subscribe_first) - public void onSubscribeFirstButtonClick() { - if (firstSubscription == null || (firstSubscription != null && firstSubscription.isUnsubscribed())) { - Log.v(TAG, "onSubscribeFirstButtonClick()"); - firstSubscription = subscribeFirst(); - } else { - Toast.makeText(getApplicationContext(), "First subscriber already started", Toast.LENGTH_SHORT).show(); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - @OnClick(R.id.btn_subscribe_second) - public void onSubscribeSecondButtonClick() { - if (secondSubscription == null || (secondSubscription != null && secondSubscription.isUnsubscribed())) { - Log.v(TAG, "onSubscribeSecondButtonClick()"); - secondSubscription = subscribeSecond(); - } else { - Toast.makeText(getApplicationContext(), "Second subscriber already started", Toast.LENGTH_SHORT).show(); - } + // We will create the ConnectableObservable in the onCreate() method, so it will be available for the two + // subscribers to subscribe to it, even before call ConnectionObservable::connect(). + connectable = Observable.interval(500, TimeUnit.MILLISECONDS).doOnNext((number) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + // This will convert the Observable to a ConnectableObservable + .publish(); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResultFirstSubscription.setText(""); + tvResultSecondSubscription.setText(""); + } + + @OnClick(R.id.btn_connect) + public void onConnectButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onConnectButtonClick()"); + resetData(); + subscription = connect(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_unsubscribe_first) - public void onUnsubscribeFirstButtonClick() { - Log.v(TAG, "onUnsubscribeFirstButtonClick()"); - unsubscribeFirst(true); - } - - @OnClick(R.id.btn_unsubscribe_second) - public void onUnsubscribeSecondButtonClick() { - Log.v(TAG, "onUnsubscribeSecondButtonClick()"); - unsubscribeSecond(true); + } + + @OnClick(R.id.btn_subscribe_first) + public void onSubscribeFirstButtonClick() { + if (firstSubscription == null || (firstSubscription != null + && firstSubscription.isUnsubscribed())) { + Log.v(TAG, "onSubscribeFirstButtonClick()"); + firstSubscription = subscribeFirst(); + } else { + Toast.makeText(getApplicationContext(), "First subscriber already started", + Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_disconnect) - public void onDisconnectButtonClick() { - Log.v(TAG, "onDisconnectButtonClick()"); - - if (subscription != null && !subscription.isUnsubscribed()) { - unsubscribeFirst(false); - unsubscribeSecond(false); - disconnect(); - } else { - Toast.makeText(getApplicationContext(), "Observable not connected", Toast.LENGTH_SHORT).show(); - } - } - - /** - * From the docs: - * - * "A Connectable Observable resembles an ordinary Observable, except that it does not begin emitting items when - * it is subscribed to, but only when its connect() method is called." - * - * This means that when user clicks on "connect" button, if there is already any subscriber subscribed to it, it will start - * receiving emitted items, otherwise, emitted items will be discarded. - * - * After connecting to the observable, new subscribers will only receive new emitted items. - * - * @return - */ - private Subscription connect() { - Log.v(TAG, "connect()"); - - // This will instruct the connectable observable to begin emitting items. If there is any subscriber subscribed to it, - // it will start receiving items. - return connectable.connect(); + } + + @OnClick(R.id.btn_subscribe_second) + public void onSubscribeSecondButtonClick() { + if (secondSubscription == null || (secondSubscription != null + && secondSubscription.isUnsubscribed())) { + Log.v(TAG, "onSubscribeSecondButtonClick()"); + secondSubscription = subscribeSecond(); + } else { + Toast.makeText(getApplicationContext(), "Second subscriber already started", + Toast.LENGTH_SHORT).show(); } - - /** - * If observable is already connected, when this button is pressed, this subscriber will start receiving items. If there is no - * connection yet, nothing will happen (until a subscription) - * - * @return - */ - private Subscription subscribeFirst() { - Log.v(TAG, "subscribeFirst()"); - - return connectable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultFirstSubscription)); - } - - /** - * If observable is already connected, when this button is pressed, this subscriber will start receiving items. If there is no - * connection yet, nothing will happen (until a subscription) - * - * @return - */ - private Subscription subscribeSecond() { - Log.v(TAG, "subscribeSecond()"); - - return connectable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultSecondSubscription)); - } - - /** - * By unsubscribing the subscription returned by the connect() method, all subscriptions will stop receiving items. - * - */ - private void disconnect() { - Log.v(TAG, "disconnect()"); - subscription.unsubscribe(); + } + + @OnClick(R.id.btn_unsubscribe_first) + public void onUnsubscribeFirstButtonClick() { + Log.v(TAG, "onUnsubscribeFirstButtonClick()"); + unsubscribeFirst(true); + } + + @OnClick(R.id.btn_unsubscribe_second) + public void onUnsubscribeSecondButtonClick() { + Log.v(TAG, "onUnsubscribeSecondButtonClick()"); + unsubscribeSecond(true); + } + + @OnClick(R.id.btn_disconnect) + public void onDisconnectButtonClick() { + Log.v(TAG, "onDisconnectButtonClick()"); + + if (subscription != null && !subscription.isUnsubscribed()) { + unsubscribeFirst(false); + unsubscribeSecond(false); + disconnect(); + } else { + Toast.makeText(getApplicationContext(), "Observable not connected", Toast.LENGTH_SHORT) + .show(); } + } + + /** + * From the docs: + * + * "A Connectable Observable resembles an ordinary Observable, except that it does not begin emitting items when + * it is subscribed to, but only when its connect() method is called." + * + * This means that when user clicks on "connect" button, if there is already any subscriber subscribed to it, it will start + * receiving emitted items, otherwise, emitted items will be discarded. + * + * After connecting to the observable, new subscribers will only receive new emitted items. + */ + private Subscription connect() { + Log.v(TAG, "connect()"); + + // This will instruct the connectable observable to begin emitting items. If there is any subscriber subscribed to it, + // it will start receiving items. + return connectable.connect(); + } + + /** + * If observable is already connected, when this button is pressed, this subscriber will start receiving items. If there is no + * connection yet, nothing will happen (until a subscription) + */ + private Subscription subscribeFirst() { + Log.v(TAG, "subscribeFirst()"); + + return connectable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultFirstSubscription)); + } + + /** + * If observable is already connected, when this button is pressed, this subscriber will start receiving items. If there is no + * connection yet, nothing will happen (until a subscription) + */ + private Subscription subscribeSecond() { + Log.v(TAG, "subscribeSecond()"); + + return connectable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultSecondSubscription)); + } + + /** + * By unsubscribing the subscription returned by the connect() method, all subscriptions will stop receiving items. + */ + private void disconnect() { + Log.v(TAG, "disconnect()"); + subscription.unsubscribe(); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableRefCountExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableRefCountExampleActivity.java index 7d8d213..cf2d43f 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableRefCountExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableRefCountExampleActivity.java @@ -5,15 +5,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -24,164 +21,163 @@ * * refCount keeps a reference to all the subscribers subscribed to it. When we call refCount, observable does not start emitting items, but only when * the first subscriber subscribe to it. - * */ public class HotObservableRefCountExampleActivity extends HotObservablesBaseActivity { - private static final String TAG = HotObservableRefCountExampleActivity.class.getSimpleName(); - - // This is the Observable returned by the refCount() method call. Subscribers must use it to subscribe. - private Observable observable; - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result_first_subscription) TextView tvResultFirstSubscription; - @BindView(R.id.tv_result_second_subscription) TextView tvResultSecondSubscription; + private static final String TAG = HotObservableRefCountExampleActivity.class.getSimpleName(); - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_hot_observable_refcount_example); - ButterKnife.bind(this); + // This is the Observable returned by the refCount() method call. Subscribers must use it to subscribe. + private Observable observable; - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result_first_subscription) + TextView tvResultFirstSubscription; + @BindView(R.id.tv_result_second_subscription) + TextView tvResultSecondSubscription; - // Just start our Hot Observable. Note that this will NOT make it to start emitting items - connectable = Observable - .interval(500, TimeUnit.MILLISECONDS) - .doOnNext((number) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_hot_observable_refcount_example); + ButterKnife.bind(this); - // This will convert the Observable to a ConnectableObservable - .publish(); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - private void resetData() { - tvEmittedNumbers.setText(""); - tvResultFirstSubscription.setText(""); - tvResultSecondSubscription.setText(""); + // Just start our Hot Observable. Note that this will NOT make it to start emitting items + connectable = Observable.interval(500, TimeUnit.MILLISECONDS).doOnNext((number) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + // This will convert the Observable to a ConnectableObservable + .publish(); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResultFirstSubscription.setText(""); + tvResultSecondSubscription.setText(""); + } + + @OnClick(R.id.btn_refcount) + public void onRefCountButtonClick() { + + if (firstSubscription == null + || (firstSubscription != null && firstSubscription.isUnsubscribed()) && (secondSubscription + == null || secondSubscription != null && secondSubscription.isUnsubscribed())) { + + Log.v(TAG, "onRefCountButtonClick()"); + resetData(); + refCount(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_refcount) - public void onRefCountButtonClick() { - - if (firstSubscription == null || (firstSubscription != null && firstSubscription.isUnsubscribed()) && - (secondSubscription == null ||secondSubscription != null && secondSubscription.isUnsubscribed())) { - - Log.v(TAG, "onRefCountButtonClick()"); - resetData(); - refCount(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_subscribe_first) + public void onSubscribeFirstButtonClick() { + if (observable == null) { + Log.v(TAG, + "onSubscribeFirstButtonClick() - Cannot start a subscriber. You must first call refCount()."); + + // When using refCount, we must subscribe our subscriber's upon the Observable returned by the refCount, and not on the + // ConnectableObservable returned by the publish() (as we do when using connect() operator). + Toast.makeText(getApplicationContext(), "You must first call refCount()", Toast.LENGTH_SHORT) + .show(); + return; } - @OnClick(R.id.btn_subscribe_first) - public void onSubscribeFirstButtonClick() { - if (observable == null) { - Log.v(TAG, "onSubscribeFirstButtonClick() - Cannot start a subscriber. You must first call refCount()."); - - // When using refCount, we must subscribe our subscriber's upon the Observable returned by the refCount, and not on the - // ConnectableObservable returned by the publish() (as we do when using connect() operator). - Toast.makeText(getApplicationContext(), "You must first call refCount()", Toast.LENGTH_SHORT).show(); - return; - } + if (firstSubscription == null || (firstSubscription != null + && firstSubscription.isUnsubscribed())) { - if (firstSubscription == null || (firstSubscription != null && firstSubscription.isUnsubscribed())) { + // Just clean up GUI in order to make things clear. + if (secondSubscription != null && secondSubscription.isUnsubscribed()) { + resetData(); + } - // Just clean up GUI in order to make things clear. - if (secondSubscription != null && secondSubscription.isUnsubscribed()) { - resetData(); - } - - Log.v(TAG, "onSubscribeFirstButtonClick()"); - firstSubscription = subscribeFirst(); - - } else { - Toast.makeText(getApplicationContext(), "First subscriber already started", Toast.LENGTH_SHORT).show(); - } + Log.v(TAG, "onSubscribeFirstButtonClick()"); + firstSubscription = subscribeFirst(); + } else { + Toast.makeText(getApplicationContext(), "First subscriber already started", + Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_subscribe_second) - public void onSubscribeSecondButtonClick() { - if (observable == null) { - Log.v(TAG, "onSubscribeSecondButtonClick() - Cannot start a subscriber. You must first call refCount()."); - - // When using refCount, we must subscribe our subscriber's upon the Observable returned by the refCount, and not on the - // ConnectableObservable returned by the publish() (as we do when using connect() operator). - Toast.makeText(getApplicationContext(), "You must first call refCount()", Toast.LENGTH_SHORT).show(); - return; - } - - if (secondSubscription == null || (secondSubscription != null && secondSubscription.isUnsubscribed())) { - - // Just clean up GUI in order to make things clear. - if (firstSubscription != null && firstSubscription.isUnsubscribed()) { - resetData(); - } - - Log.v(TAG, "onSubscribeSecondButtonClick()"); - secondSubscription = subscribeSecond(); - - } else { - Toast.makeText(getApplicationContext(), "Second subscriber already started", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_subscribe_second) + public void onSubscribeSecondButtonClick() { + if (observable == null) { + Log.v(TAG, + "onSubscribeSecondButtonClick() - Cannot start a subscriber. You must first call refCount()."); + + // When using refCount, we must subscribe our subscriber's upon the Observable returned by the refCount, and not on the + // ConnectableObservable returned by the publish() (as we do when using connect() operator). + Toast.makeText(getApplicationContext(), "You must first call refCount()", Toast.LENGTH_SHORT) + .show(); + return; } - /** - * When unsubscribing a subscriber, if there is no more subscriber subscribed to the observable, it will stop emit items. - * - */ - @OnClick(R.id.btn_unsubscribe_first) - public void onUnsubscribeFirstButtonClick() { - Log.v(TAG, "onUnsubscribeFirstButtonClick()"); - unsubscribeFirst(true); - } - - /** - * When unsubscribing a subscriber, if there is no more subscriber subscribed to the observable, it will stop emit items. - * - */ - @OnClick(R.id.btn_unsubscribe_second) - public void onUnsubscribeSecondButtonClick() { - Log.v(TAG, "onUnsubscribeSecondButtonClick()"); - unsubscribeSecond(true); - } - - private void refCount() { - Log.v(TAG, "refCount()"); + if (secondSubscription == null || (secondSubscription != null + && secondSubscription.isUnsubscribed())) { - // refCount returns Observable that is connected as long as there are subscribers to it. - observable = connectable.refCount(); - } - - /** - * If this is the first subscriber to subscribe to the observable, it will make observable to start emitting items. - * - * @return - */ - private Subscription subscribeFirst() { - Log.v(TAG, "subscribeFirst()"); - - return observable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultFirstSubscription)); - } + // Just clean up GUI in order to make things clear. + if (firstSubscription != null && firstSubscription.isUnsubscribed()) { + resetData(); + } - /** - * If this is the first subscriber to subscribe to the observable, it will make observable to start emitting items. - * - * @return - */ - private Subscription subscribeSecond() { - Log.v(TAG, "subscribeSecond()"); - - return observable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultSecondSubscription)); + Log.v(TAG, "onSubscribeSecondButtonClick()"); + secondSubscription = subscribeSecond(); + } else { + Toast.makeText(getApplicationContext(), "Second subscriber already started", + Toast.LENGTH_SHORT).show(); } + } + + /** + * When unsubscribing a subscriber, if there is no more subscriber subscribed to the observable, it will stop emit items. + */ + @OnClick(R.id.btn_unsubscribe_first) + public void onUnsubscribeFirstButtonClick() { + Log.v(TAG, "onUnsubscribeFirstButtonClick()"); + unsubscribeFirst(true); + } + + /** + * When unsubscribing a subscriber, if there is no more subscriber subscribed to the observable, it will stop emit items. + */ + @OnClick(R.id.btn_unsubscribe_second) + public void onUnsubscribeSecondButtonClick() { + Log.v(TAG, "onUnsubscribeSecondButtonClick()"); + unsubscribeSecond(true); + } + + private void refCount() { + Log.v(TAG, "refCount()"); + + // refCount returns Observable that is connected as long as there are subscribers to it. + observable = connectable.refCount(); + } + + /** + * If this is the first subscriber to subscribe to the observable, it will make observable to start emitting items. + */ + private Subscription subscribeFirst() { + Log.v(TAG, "subscribeFirst()"); + + return observable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultFirstSubscription)); + } + + /** + * If this is the first subscriber to subscribe to the observable, it will make observable to start emitting items. + */ + private Subscription subscribeSecond() { + Log.v(TAG, "subscribeSecond()"); + + return observable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultSecondSubscription)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableReplayExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableReplayExampleActivity.java index dcf6a68..0bfe2fa 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableReplayExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/hotobservables/HotObservableReplayExampleActivity.java @@ -5,15 +5,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.HotObservablesBaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -26,259 +23,245 @@ * collected items might vary) * * Later, as we subscribe our subscribers, it will "replay" all collected items. - * */ public class HotObservableReplayExampleActivity extends HotObservablesBaseActivity { - private static final String TAG = HotObservableReplayExampleActivity.class.getSimpleName(); + private static final String TAG = HotObservableReplayExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result_first_subscription) TextView tvResultFirstSubscription; - @BindView(R.id.tv_result_second_subscription) TextView tvResultSecondSubscription; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result_first_subscription) + TextView tvResultFirstSubscription; + @BindView(R.id.tv_result_second_subscription) + TextView tvResultSecondSubscription; - private boolean replyOk = false; + private boolean replyOk = false; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_hot_observable_replay_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_hot_observable_replay_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResultFirstSubscription.setText(""); - tvResultSecondSubscription.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResultFirstSubscription.setText(""); + tvResultSecondSubscription.setText(""); + } + + @OnClick(R.id.btn_replay) + public void onReplayButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onReplayButtonClick()"); + resetData(); + replay(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_replay) - public void onReplayButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onReplayButtonClick()"); - resetData(); - replay(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_replay_with_buffer_size) + public void onReplayWithBufferSizeButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onReplayWithBufferSizeButtonClick()"); + resetData(); + replayWithBufferSize(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_replay_with_buffer_size) - public void onReplayWithBufferSizeButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onReplayWithBufferSizeButtonClick()"); - resetData(); - replayWithBufferSize(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_replay_with_time) + public void onReplayWithTimeButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onReplayWithTimeButtonClick()"); + resetData(); + replayWithTime(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_replay_with_time) - public void onReplayWithTimeButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onReplayWithTimeButtonClick()"); - resetData(); - replayWithTime(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } + @OnClick(R.id.btn_connect) + public void onConnectButtonClick() { - @OnClick(R.id.btn_connect) - public void onConnectButtonClick() { - - if (!replyOk) { - Toast.makeText(getApplicationContext(), "Please, choose one replay option prior to connect.", Toast.LENGTH_SHORT).show(); - return; - } - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onConnectButtonClick()"); - resetData(); - subscription = connect(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + if (!replyOk) { + Toast.makeText(getApplicationContext(), "Please, choose one replay option prior to connect.", + Toast.LENGTH_SHORT).show(); + return; } - @OnClick(R.id.btn_subscribe_first) - public void onSubscribeFirstButtonClick() { - if (!replyOk) { - Toast.makeText(getApplicationContext(), "Please, choose one replay option prior to subscribe.", Toast.LENGTH_SHORT).show(); - return; - } - - if (firstSubscription == null || (firstSubscription != null && firstSubscription.isUnsubscribed())) { - Log.v(TAG, "onSubscribeFirstButtonClick()"); - firstSubscription = subscribeFirst(); - } else { - Toast.makeText(getApplicationContext(), "First subscriber already started", Toast.LENGTH_SHORT).show(); - } + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onConnectButtonClick()"); + resetData(); + subscription = connect(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - - @OnClick(R.id.btn_subscribe_second) - public void onSubscribeSecondButtonClick() { - if (!replyOk) { - Toast.makeText(getApplicationContext(), "Please, choose one replay option prior to subscribe.", Toast.LENGTH_SHORT).show(); - return; - } - - if (secondSubscription == null || (secondSubscription != null && secondSubscription.isUnsubscribed())) { - Log.v(TAG, "onSubscribeSecondButtonClick()"); - secondSubscription = subscribeSecond(); - } else { - Toast.makeText(getApplicationContext(), "Second subscriber already started", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_subscribe_first) + public void onSubscribeFirstButtonClick() { + if (!replyOk) { + Toast.makeText(getApplicationContext(), + "Please, choose one replay option prior to subscribe.", Toast.LENGTH_SHORT).show(); + return; } - @OnClick(R.id.btn_unsubscribe_first) - public void onUnsubscribeFirstButtonClick() { - Log.v(TAG, "onUnsubscribeFirstButtonClick()"); - unsubscribeFirst(true); + if (firstSubscription == null || (firstSubscription != null + && firstSubscription.isUnsubscribed())) { + Log.v(TAG, "onSubscribeFirstButtonClick()"); + firstSubscription = subscribeFirst(); + } else { + Toast.makeText(getApplicationContext(), "First subscriber already started", + Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_unsubscribe_second) - public void onUnsubscribeSecondButtonClick() { - Log.v(TAG, "onUnsubscribeSecondButtonClick()"); - unsubscribeSecond(true); - } - - @OnClick(R.id.btn_disconnect) - public void onDisconnectButtonClick() { - Log.v(TAG, "onDisconnectButtonClick()"); - - if (subscription != null && !subscription.isUnsubscribed()) { - unsubscribeFirst(false); - unsubscribeSecond(false); - disconnect(); - } else { - Toast.makeText(getApplicationContext(), "Observable not connected", Toast.LENGTH_SHORT).show(); - } - } - - /** - * When calling replay, observable will start emit and replay() operator will collect them. - * - * @return - */ - private void replay() { - Log.v(TAG, "replay()"); - - // First create our observable. Later, when calling connect, it will start emitting items and collecting them. - connectable = Observable - .interval(500, TimeUnit.MILLISECONDS) - .doOnNext((number) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - .replay(); - - // Used only to prevent user to click on the connect button prior to choose one of the replay() approaches. - replyOk = true; - } - - /** - * When calling replay(bufferSize), observable will start emit and collect items. It will replay at most [bufferSize] items - * emitted by the observable. - * - * @return - */ - private void replayWithBufferSize() { - Log.v(TAG, "replayWithBufferSize(5)"); - - // First create our observable. Later, when calling connect, it will start emitting items and collecting at most N them. - connectable = Observable - .interval(500, TimeUnit.MILLISECONDS) - .doOnNext((number) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - - // Replay the last 5 emitted items. - .replay(5); - - // Used only to prevent user to click on the connect button prior to choose one of the replay() approaches. - replyOk = true; - } - - /** - * When calling replay(time), observable will start emit and collect items. Upon subscription, it will replay all items emitted - * by the observable within a specified time window. - * - * @return - */ - private void replayWithTime() { - Log.v(TAG, "replayWithTime(6 seconds)"); - - // First create our observable. Later, when calling connect, it will start emitting items and collecting them. - connectable = Observable - .interval(500, TimeUnit.MILLISECONDS) - .doOnNext((number) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - - // Replay all items emitted by the observable within this time window. - .replay(6, TimeUnit.SECONDS); - - // Used only to prevent user to click on the connect button prior to choose one of the replay() approaches. - replyOk = true; - } - - /** - * When this button is pressed, observable will start emitting items. If there is already a subscription, it will start receiving - * emitted items. Otherwise emitted items will be collected and after a subscription, they will be replayed. - * - * @return - */ - private Subscription connect() { - Log.v(TAG, "connect()"); - return connectable.connect(); - } - - /** - * If we are already connected to the observable, when this button is pressed, all emitted items will be replayed (depends on - * which replay variant we are testing, replayed items might vary). - * - * @return - */ - private Subscription subscribeFirst() { - Log.v(TAG, "subscribeFirst()"); - - return connectable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultFirstSubscription)); + } + + @OnClick(R.id.btn_subscribe_second) + public void onSubscribeSecondButtonClick() { + if (!replyOk) { + Toast.makeText(getApplicationContext(), + "Please, choose one replay option prior to subscribe.", Toast.LENGTH_SHORT).show(); + return; } - /** - * If we are already connected to the observable, when this button is pressed, all emitted items will be replayed (depends on - * which replay variant we are testing, replayed items might vary). - * - * @return - */ - private Subscription subscribeSecond() { - Log.v(TAG, "subscribeSecond()"); - - return connectable - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResultSecondSubscription)); + if (secondSubscription == null || (secondSubscription != null + && secondSubscription.isUnsubscribed())) { + Log.v(TAG, "onSubscribeSecondButtonClick()"); + secondSubscription = subscribeSecond(); + } else { + Toast.makeText(getApplicationContext(), "Second subscriber already started", + Toast.LENGTH_SHORT).show(); } - - /** - * By unsubscribing the subscriber returned by the connect() method, all subscriptions will stop receiving items. - * - */ - private void disconnect() { - Log.v(TAG, "disconnect()"); - subscription.unsubscribe(); - - replyOk = false; - + } + + @OnClick(R.id.btn_unsubscribe_first) + public void onUnsubscribeFirstButtonClick() { + Log.v(TAG, "onUnsubscribeFirstButtonClick()"); + unsubscribeFirst(true); + } + + @OnClick(R.id.btn_unsubscribe_second) + public void onUnsubscribeSecondButtonClick() { + Log.v(TAG, "onUnsubscribeSecondButtonClick()"); + unsubscribeSecond(true); + } + + @OnClick(R.id.btn_disconnect) + public void onDisconnectButtonClick() { + Log.v(TAG, "onDisconnectButtonClick()"); + + if (subscription != null && !subscription.isUnsubscribed()) { + unsubscribeFirst(false); + unsubscribeSecond(false); + disconnect(); + } else { + Toast.makeText(getApplicationContext(), "Observable not connected", Toast.LENGTH_SHORT) + .show(); } + } + + /** + * When calling replay, observable will start emit and replay() operator will collect them. + */ + private void replay() { + Log.v(TAG, "replay()"); + + // First create our observable. Later, when calling connect, it will start emitting items and collecting them. + connectable = Observable.interval(500, TimeUnit.MILLISECONDS).doOnNext((number) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }).replay(); + + // Used only to prevent user to click on the connect button prior to choose one of the replay() approaches. + replyOk = true; + } + + /** + * When calling replay(bufferSize), observable will start emit and collect items. It will replay at most [bufferSize] items + * emitted by the observable. + */ + private void replayWithBufferSize() { + Log.v(TAG, "replayWithBufferSize(5)"); + + // First create our observable. Later, when calling connect, it will start emitting items and collecting at most N them. + connectable = Observable.interval(500, TimeUnit.MILLISECONDS).doOnNext((number) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + // Replay the last 5 emitted items. + .replay(5); + + // Used only to prevent user to click on the connect button prior to choose one of the replay() approaches. + replyOk = true; + } + + /** + * When calling replay(time), observable will start emit and collect items. Upon subscription, it will replay all items emitted + * by the observable within a specified time window. + */ + private void replayWithTime() { + Log.v(TAG, "replayWithTime(6 seconds)"); + + // First create our observable. Later, when calling connect, it will start emitting items and collecting them. + connectable = Observable.interval(500, TimeUnit.MILLISECONDS).doOnNext((number) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + + // Replay all items emitted by the observable within this time window. + .replay(6, TimeUnit.SECONDS); + + // Used only to prevent user to click on the connect button prior to choose one of the replay() approaches. + replyOk = true; + } + + /** + * When this button is pressed, observable will start emitting items. If there is already a subscription, it will start receiving + * emitted items. Otherwise emitted items will be collected and after a subscription, they will be replayed. + */ + private Subscription connect() { + Log.v(TAG, "connect()"); + return connectable.connect(); + } + + /** + * If we are already connected to the observable, when this button is pressed, all emitted items will be replayed (depends on + * which replay variant we are testing, replayed items might vary). + */ + private Subscription subscribeFirst() { + Log.v(TAG, "subscribeFirst()"); + + return connectable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultFirstSubscription)); + } + + /** + * If we are already connected to the observable, when this button is pressed, all emitted items will be replayed (depends on + * which replay variant we are testing, replayed items might vary). + */ + private Subscription subscribeSecond() { + Log.v(TAG, "subscribeSecond()"); + + return connectable.compose(applySchedulers()) + .subscribe(resultSubscriber(tvResultSecondSubscription)); + } + + /** + * By unsubscribing the subscriber returned by the connect() method, all subscriptions will stop receiving items. + */ + private void disconnect() { + Log.v(TAG, "disconnect()"); + subscription.unsubscribe(); + + replyOk = false; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/main/CategoryItem.java b/app/src/main/java/com/motondon/rxjavademoapp/view/main/CategoryItem.java index 0a6454c..59d1e1b 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/main/CategoryItem.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/main/CategoryItem.java @@ -1,24 +1,22 @@ package com.motondon.rxjavademoapp.view.main; import android.app.Activity; - import java.io.Serializable; /** * Pair consisting of the name of an example and the activity corresponding to the example. - * */ public class CategoryItem implements Serializable { - public final Class mExampleActivityClass; - public final String mExampleName; - public final String mExampleDetails; + public final Class mExampleActivityClass; + public final String mExampleName; + public final String mExampleDetails; - public CategoryItem( - Class exampleActivityClass, String exampleName, String exampleDetails) { - mExampleActivityClass = exampleActivityClass; - mExampleName = exampleName; - mExampleDetails = exampleDetails; - } + public CategoryItem(Class exampleActivityClass, String exampleName, + String exampleDetails) { + mExampleActivityClass = exampleActivityClass; + mExampleName = exampleName; + mExampleDetails = exampleDetails; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/main/ExampleByCategoryActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/main/ExampleByCategoryActivity.java index 961595a..194785f 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/main/ExampleByCategoryActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/main/ExampleByCategoryActivity.java @@ -5,54 +5,54 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.adapter.CategoryItemsAdapter; - import java.util.ArrayList; -import butterknife.BindView; -import butterknife.ButterKnife; - public class ExampleByCategoryActivity extends AppCompatActivity { - @BindView(R.id.example_by_category_list) RecyclerView examplesByCategoryList; - - private String title; - private ArrayList categoryItems; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_example_by_category); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - - if (savedInstanceState != null) { - title = savedInstanceState.getString("TITLE"); - categoryItems = (ArrayList) savedInstanceState.getSerializable("CATEGORY_ITEMS"); - } else { - title = getIntent().getExtras().getString("TITLE"); - - // Get the examples lists from the intent and set it to the adapter. They will be available to users and will allow them to - // click over an option and be redirected to an activity which implements that example. - categoryItems = (ArrayList) getIntent().getExtras().getSerializable("CATEGORY_ITEMS"); - } - actionBar.setTitle(title); - } - - examplesByCategoryList.setHasFixedSize(true); - examplesByCategoryList.setLayoutManager(new LinearLayoutManager(this)); - examplesByCategoryList.setAdapter(new CategoryItemsAdapter(this, categoryItems)); + @BindView(R.id.example_by_category_list) + RecyclerView examplesByCategoryList; + + private String title; + private ArrayList categoryItems; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_example_by_category); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + + if (savedInstanceState != null) { + title = savedInstanceState.getString("TITLE"); + categoryItems = + (ArrayList) savedInstanceState.getSerializable("CATEGORY_ITEMS"); + } else { + title = getIntent().getExtras().getString("TITLE"); + + // Get the examples lists from the intent and set it to the adapter. They will be available to users and will allow them to + // click over an option and be redirected to an activity which implements that example. + categoryItems = + (ArrayList) getIntent().getExtras().getSerializable("CATEGORY_ITEMS"); + } + actionBar.setTitle(title); } - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); + examplesByCategoryList.setHasFixedSize(true); + examplesByCategoryList.setLayoutManager(new LinearLayoutManager(this)); + examplesByCategoryList.setAdapter(new CategoryItemsAdapter(this, categoryItems)); + } - outState.putString("TITLE", title); - outState.putSerializable("CATEGORY_ITEMS", categoryItems); - } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString("TITLE", title); + outState.putSerializable("CATEGORY_ITEMS", categoryItems); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/main/MainActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/main/MainActivity.java index bebdc37..486724c 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/main/MainActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/main/MainActivity.java @@ -6,15 +6,18 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; - +import butterknife.BindView; +import butterknife.ButterKnife; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.adapter.MainActivityAdapter; -import com.motondon.rxjavademoapp.view.backpressure.BackpressureManualRequestExampleActivity; import com.motondon.rxjavademoapp.view.backpressure.BackpressureBasicExampleActivity; +import com.motondon.rxjavademoapp.view.backpressure.BackpressureManualRequestExampleActivity; import com.motondon.rxjavademoapp.view.backpressure.BackpressureReactivePullExampleActivity; import com.motondon.rxjavademoapp.view.backpressure.BackpressureSpecificOperatorsExampleActivity; import com.motondon.rxjavademoapp.view.generalexamples.BroadcastSystemStatusExampleActivity; import com.motondon.rxjavademoapp.view.generalexamples.DrawingExampleActivity; +import com.motondon.rxjavademoapp.view.generalexamples.ServerPollingAfterDataProcessingExampleActivity; +import com.motondon.rxjavademoapp.view.generalexamples.ServerPollingExampleActivity; import com.motondon.rxjavademoapp.view.generalexamples.TypingIndicatorExampleActivity; import com.motondon.rxjavademoapp.view.hotobservables.HotObservableCacheExampleActivity; import com.motondon.rxjavademoapp.view.hotobservables.HotObservableConnectExampleActivity; @@ -32,219 +35,176 @@ import com.motondon.rxjavademoapp.view.operators.TimeoutExampleActivity; import com.motondon.rxjavademoapp.view.operators.TransformingOperatorsExampleActivity; import com.motondon.rxjavademoapp.view.parallelization.ParallelizationExampleActivity; -import com.motondon.rxjavademoapp.view.generalexamples.ServerPollingAfterDataProcessingExampleActivity; -import com.motondon.rxjavademoapp.view.generalexamples.ServerPollingExampleActivity; - import java.util.ArrayList; import java.util.List; -import butterknife.BindView; -import butterknife.ButterKnife; - public class MainActivity extends AppCompatActivity { - @BindView(R.id.category_list) RecyclerView exampleCategoriesList; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - ButterKnife.bind(this); + @BindView(R.id.category_list) + RecyclerView exampleCategoriesList; - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(R.string.example_list_title); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ButterKnife.bind(this); - exampleCategoriesList.setHasFixedSize(true); - exampleCategoriesList.setLayoutManager(new LinearLayoutManager(this)); - exampleCategoriesList.setAdapter(new MainActivityAdapter(this, getCategoriesList())); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.example_list_title); } - /** - * This method is intended to hold a list of a Pair object that contains: - * - First arg: List of CategoryItem - * - Second arg: Pair of String, String - * - * @return - */ - private List, Pair>> getCategoriesList() { - List, Pair>> exampleTypesList = new ArrayList<>(); - - exampleTypesList.add(new Pair<>(getOperatorsCategoryExamples(), - new Pair<>( - "RxJava Operators Examples", - "Contain examples of RxJava operators such as filtering, combining, conditional, etc."))); - exampleTypesList.add(new Pair<>(getBackpressureCategoryExamples(), - new Pair<>( - "Backpressure Examples", - "Contain examples of how to deal with backpressure issues by presenting some strategies in order to alleviate it."))); - exampleTypesList.add(new Pair<>(getHotObservablesCategoryExamples(), - new Pair<>( - "Hot Observable Examples", - "Contain examples of hot observables."))); - exampleTypesList.add(new Pair<>(getParallelizationCategoryExamples(), - new Pair<>( - "Parallelization", - "Contain examples of how to implement parallelization using different approaches."))); - exampleTypesList.add(new Pair<>(getGeneralCategoryExamples(), - new Pair<>( - "General Examples", - "This option includes some general examples such as polling server data, some examples on how to deal with UI components in a reactive way, etc."))); - return exampleTypesList; - } + exampleCategoriesList.setHasFixedSize(true); + exampleCategoriesList.setLayoutManager(new LinearLayoutManager(this)); + exampleCategoriesList.setAdapter(new MainActivityAdapter(this, getCategoriesList())); + } + + /** + * This method is intended to hold a list of a Pair object that contains: + * - First arg: List of CategoryItem + * - Second arg: Pair of String, String + */ + private List, Pair>> getCategoriesList() { + List, Pair>> exampleTypesList = new ArrayList<>(); + + exampleTypesList.add(new Pair<>(getOperatorsCategoryExamples(), + new Pair<>("RxJava Operators Examples", + "Contain examples of RxJava operators such as filtering, combining, conditional, etc."))); + exampleTypesList.add(new Pair<>(getBackpressureCategoryExamples(), + new Pair<>("Backpressure Examples", + "Contain examples of how to deal with backpressure issues by presenting some strategies in order to alleviate it."))); + exampleTypesList.add(new Pair<>(getHotObservablesCategoryExamples(), + new Pair<>("Hot Observable Examples", "Contain examples of hot observables."))); + exampleTypesList.add(new Pair<>(getParallelizationCategoryExamples(), + new Pair<>("Parallelization", + "Contain examples of how to implement parallelization using different approaches."))); + exampleTypesList.add(new Pair<>(getGeneralCategoryExamples(), new Pair<>("General Examples", + "This option includes some general examples such as polling server data, some examples on how to deal with UI components in a reactive way, etc."))); + return exampleTypesList; + } - private List getOperatorsCategoryExamples() { - List operatorsExamples = new ArrayList<>(); - - operatorsExamples.add(new CategoryItem( - FilteringExampleActivity.class, - "Filtering operators", - "Show some operators which filter out emitted items. They are: first(), firstOrDefault(), takeFirst(), single(), singleOrDefault(), elementAt(), last(), lastOrDefault(), take(), takeLast() and filter().")); - - operatorsExamples.add(new CategoryItem( - MoreFilteringOperatorsExampleActivity.class, - "More about filtering operators", - "Show some other filtering operators, such as sample(), sample(w/ Observable), debounce(), throttleLast(), etc.")); - - operatorsExamples.add(new CategoryItem( - CombiningObservablesExampleActivity.class, - "Combining Operators", - "Show operators that are used to combine emissions (zip(), merge(), etc).")); - - operatorsExamples.add(new CategoryItem( - ConditionalOperatorsExampleActivity.class, - "Conditional Operators", - "Demonstrate some conditional operators like skipWhile(), skipUntil(), etc.")); - - operatorsExamples.add(new CategoryItem( - TransformingOperatorsExampleActivity.class, - "Transforming Operators", - "Show different variations of buffer() operator as well as windows() and scan().")); - - operatorsExamples.add(new CategoryItem( - TimeoutExampleActivity.class, - "Timeout", - "Implement different variations of timeout() operator.")); - - operatorsExamples.add(new CategoryItem( - AggregateOperatorsExampleActivity.class, - "Aggregate Operators", - "Show operators that fit on the aggregate category such as collect(), reduce(), etc.")); - - operatorsExamples.add(new CategoryItem( - JoinExampleActivity.class, - "Join", - "Show join() operator by allowing users to set different values and quick check how join() behaves.")); - - operatorsExamples.add(new CategoryItem( - RetryExampleActivity.class, - "Retry and RetryWhen", - "Simulate network requests errors and use retry() and retryWhen() operators in order to retry the requests by using different approaches.")); - - operatorsExamples.add(new CategoryItem( - ConcatMapAndFlatMapExampleActivity.class, - "ConcatMap and FlatMap", - "Show how they differ from each other. Basically concatMap() cares about ordering while flatMap() does not.")); - - operatorsExamples.add(new CategoryItem( - ErrorHandlingExampleActivity.class, - "Error Handling", - "Show how to deal with error handling in the reactive way. It implements examples of checked and unchecked exceptions, as well as onErrorReturn(), onErrorResumeNext() and onExceptionResumeNext() operators.")); - - return operatorsExamples; - } + private List getOperatorsCategoryExamples() { + List operatorsExamples = new ArrayList<>(); - private List getHotObservablesCategoryExamples() { - List hotObservablesExample = new ArrayList<>(); + operatorsExamples.add(new CategoryItem(FilteringExampleActivity.class, "Filtering operators", + "Show some operators which filter out emitted items. They are: first(), firstOrDefault(), takeFirst(), single(), singleOrDefault(), elementAt(), last(), lastOrDefault(), take(), takeLast() and filter().")); - hotObservablesExample.add(new CategoryItem( - HotObservableConnectExampleActivity.class, - "Hot Observables - Connect", - "Show how to use connect() operator.")); + operatorsExamples.add(new CategoryItem(MoreFilteringOperatorsExampleActivity.class, + "More about filtering operators", + "Show some other filtering operators, such as sample(), sample(w/ Observable), debounce(), throttleLast(), etc.")); + + operatorsExamples.add( + new CategoryItem(CombiningObservablesExampleActivity.class, "Combining Operators", + "Show operators that are used to combine emissions (zip(), merge(), etc).")); + + operatorsExamples.add( + new CategoryItem(ConditionalOperatorsExampleActivity.class, "Conditional Operators", + "Demonstrate some conditional operators like skipWhile(), skipUntil(), etc.")); + + operatorsExamples.add( + new CategoryItem(TransformingOperatorsExampleActivity.class, "Transforming Operators", + "Show different variations of buffer() operator as well as windows() and scan().")); + + operatorsExamples.add(new CategoryItem(TimeoutExampleActivity.class, "Timeout", + "Implement different variations of timeout() operator.")); + + operatorsExamples.add( + new CategoryItem(AggregateOperatorsExampleActivity.class, "Aggregate Operators", + "Show operators that fit on the aggregate category such as collect(), reduce(), etc.")); + + operatorsExamples.add(new CategoryItem(JoinExampleActivity.class, "Join", + "Show join() operator by allowing users to set different values and quick check how join() behaves.")); + + operatorsExamples.add(new CategoryItem(RetryExampleActivity.class, "Retry and RetryWhen", + "Simulate network requests errors and use retry() and retryWhen() operators in order to retry the requests by using different approaches.")); + + operatorsExamples.add( + new CategoryItem(ConcatMapAndFlatMapExampleActivity.class, "ConcatMap and FlatMap", + "Show how they differ from each other. Basically concatMap() cares about ordering while flatMap() does not.")); + + operatorsExamples.add(new CategoryItem(ErrorHandlingExampleActivity.class, "Error Handling", + "Show how to deal with error handling in the reactive way. It implements examples of checked and unchecked exceptions, as well as onErrorReturn(), onErrorResumeNext() and onExceptionResumeNext() operators.")); + + return operatorsExamples; + } + + private List getHotObservablesCategoryExamples() { + List hotObservablesExample = new ArrayList<>(); + + hotObservablesExample.add( + new CategoryItem(HotObservableConnectExampleActivity.class, "Hot Observables - Connect", + "Show how to use connect() operator.")); + + hotObservablesExample.add( + new CategoryItem(HotObservableRefCountExampleActivity.class, "Hot Observables - RefCount", + "Show how to use refCount() operator.")); + + hotObservablesExample.add( + new CategoryItem(HotObservableReplayExampleActivity.class, "Hot Observables - Replay", + "Show how to use some replay() operator variants.")); + + hotObservablesExample.add( + new CategoryItem(HotObservableCacheExampleActivity.class, "Hot Observables - Cache", + "Show how to use cache() operator.")); + + return hotObservablesExample; + } + + private List getBackpressureCategoryExamples() { + List backpressureExample = new ArrayList<>(); + + backpressureExample.add(new CategoryItem(BackpressureBasicExampleActivity.class, + "Backpressure - MissingBackpressureException and throttle()", + "Show what happens when an observable produces items much faster than they are consumed by the observers.")); + + backpressureExample.add(new CategoryItem(BackpressureSpecificOperatorsExampleActivity.class, + "Backpressure - Specifc Operators", + "Show how to use backpressureBuffer() and backpressureDrop() operators.")); + + backpressureExample.add(new CategoryItem(BackpressureReactivePullExampleActivity.class, + "Backpressure - Pull Request", + "Show how to deal with backpressure by using Subscriber::request() method.")); - hotObservablesExample.add(new CategoryItem( - HotObservableRefCountExampleActivity.class, - "Hot Observables - RefCount", - "Show how to use refCount() operator.")); + backpressureExample.add(new CategoryItem(BackpressureManualRequestExampleActivity.class, + "Backpressure - Manual Request", + "Show how to use request() operator in order to request items manually.")); - hotObservablesExample.add(new CategoryItem( - HotObservableReplayExampleActivity.class, - "Hot Observables - Replay", - "Show how to use some replay() operator variants.")); + return backpressureExample; + } - hotObservablesExample.add(new CategoryItem( - HotObservableCacheExampleActivity.class, - "Hot Observables - Cache", - "Show how to use cache() operator.")); + private List getParallelizationCategoryExamples() { + List parallelizationExamples = new ArrayList<>(); - return hotObservablesExample; - } + parallelizationExamples.add( + new CategoryItem(ParallelizationExampleActivity.class, "Parallelization", + "Show how to implement parallelization in RxJava")); - private List getBackpressureCategoryExamples() { - List backpressureExample = new ArrayList<>(); + return parallelizationExamples; + } - backpressureExample.add(new CategoryItem( - BackpressureBasicExampleActivity.class, - "Backpressure - MissingBackpressureException and throttle()", - "Show what happens when an observable produces items much faster than they are consumed by the observers.")); + private List getGeneralCategoryExamples() { + List activityAndNameHelpers = new ArrayList<>(); - backpressureExample.add(new CategoryItem( - BackpressureSpecificOperatorsExampleActivity.class, - "Backpressure - Specifc Operators", - "Show how to use backpressureBuffer() and backpressureDrop() operators.")); + activityAndNameHelpers.add( + new CategoryItem(ServerPollingExampleActivity.class, "Simulate Server Polling", + "This example simulates a server polling by using different operators. Basically it analyzes each data received by the server and check whether it can be considered done or not. Depends on the example, it can poll the server until data is (considered) done or just for a fixed number of times.")); - backpressureExample.add(new CategoryItem( - BackpressureReactivePullExampleActivity.class, - "Backpressure - Pull Request", - "Show how to deal with backpressure by using Subscriber::request() method.")); + activityAndNameHelpers.add( + new CategoryItem(ServerPollingAfterDataProcessingExampleActivity.class, + "Server Polling After Local Data Processing", + "This example also shows a server polling, but it differs from the previous example in a way it simulates a local data processing, and only then it polls the server again, no matter how long it takes to process that data locally.")); - backpressureExample.add(new CategoryItem( - BackpressureManualRequestExampleActivity.class, - "Backpressure - Manual Request", - "Show how to use request() operator in order to request items manually.")); + activityAndNameHelpers.add( + new CategoryItem(TypingIndicatorExampleActivity.class, "Typing Indicator", + "Show how to implement 'typing indicator' feature by using RxJava")); - return backpressureExample; - } + activityAndNameHelpers.add( + new CategoryItem(DrawingExampleActivity.class, "Reactive Drawing Example", + "Show how to react to mouse events by using RxBindings library. Also demonstrates how to listen for SeekBar and menu item click events.")); - private List getParallelizationCategoryExamples() { - List parallelizationExamples = new ArrayList<>(); - - parallelizationExamples.add(new CategoryItem( - ParallelizationExampleActivity.class, - "Parallelization", - "Show how to implement parallelization in RxJava")); - - return parallelizationExamples; - } - - private List getGeneralCategoryExamples() { - List activityAndNameHelpers = new ArrayList<>(); - - activityAndNameHelpers.add(new CategoryItem( - ServerPollingExampleActivity.class, - "Simulate Server Polling", - "This example simulates a server polling by using different operators. Basically it analyzes each data received by the server and check whether it can be considered done or not. Depends on the example, it can poll the server until data is (considered) done or just for a fixed number of times.")); - - activityAndNameHelpers.add(new CategoryItem( - ServerPollingAfterDataProcessingExampleActivity.class, - "Server Polling After Local Data Processing", - "This example also shows a server polling, but it differs from the previous example in a way it simulates a local data processing, and only then it polls the server again, no matter how long it takes to process that data locally.")); - - activityAndNameHelpers.add(new CategoryItem( - TypingIndicatorExampleActivity.class, - "Typing Indicator", - "Show how to implement 'typing indicator' feature by using RxJava")); - - activityAndNameHelpers.add(new CategoryItem( - DrawingExampleActivity.class, - "Reactive Drawing Example", - "Show how to react to mouse events by using RxBindings library. Also demonstrates how to listen for SeekBar and menu item click events.")); - - activityAndNameHelpers.add(new CategoryItem( - BroadcastSystemStatusExampleActivity.class, - "Broadcast System Status", - "Show how to listen for broadcast system messages by using fromBroadcast() method from RxBroadcast library.")); - return activityAndNameHelpers; - } + activityAndNameHelpers.add( + new CategoryItem(BroadcastSystemStatusExampleActivity.class, "Broadcast System Status", + "Show how to listen for broadcast system messages by using fromBroadcast() method from RxBroadcast library.")); + return activityAndNameHelpers; + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/AggregateOperatorsExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/AggregateOperatorsExampleActivity.java index 011594f..d06d7f2 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/AggregateOperatorsExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/AggregateOperatorsExampleActivity.java @@ -5,17 +5,14 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.ArrayList; import java.util.List; import java.util.Random; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -25,328 +22,322 @@ public class AggregateOperatorsExampleActivity extends BaseActivity { - private static final String TAG = AggregateOperatorsExampleActivity.class.getSimpleName(); + private static final String TAG = AggregateOperatorsExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_aggregate_operators_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_aggregate_operators_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_start_sum_operator_test) + public void onStartSumOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartSumOperatorButtonClick()"); + resetData(); + subscription = startSumOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_sum_operator_test) - public void onStartSumOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartSumOperatorButtonClick()"); - resetData(); - subscription = startSumOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_max_operator_test) + public void onStartMaxOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartMaxOperatorButtonClick()"); + resetData(); + subscription = startMaxOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_max_operator_test) - public void onStartMaxOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartMaxOperatorButtonClick()"); - resetData(); - subscription = startMaxOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_collect_with_empty_statefactory_test) + public void onStartCollectTestWithEmptyStateFactoryButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartCollectTestWithEmptyStateFactoryButtonClick()"); + resetData(); + subscription = startCollectTestWithEmptyStateFactory(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_collect_with_empty_statefactory_test) - public void onStartCollectTestWithEmptyStateFactoryButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartCollectTestWithEmptyStateFactoryButtonClick()"); - resetData(); - subscription = startCollectTestWithEmptyStateFactory(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_collect_test_with_statefactory_with_values) + public void onStartCollectTestWithStateFactoryWithValuesButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartCollectTestWithStateFactoryWithValuesButtonClick()"); + resetData(); + subscription = startCollectTestWithStateFactoryWithValues(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_collect_test_with_statefactory_with_values) - public void onStartCollectTestWithStateFactoryWithValuesButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartCollectTestWithStateFactoryWithValuesButtonClick()"); - resetData(); - subscription = startCollectTestWithStateFactoryWithValues(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_reduce_test) + public void onStartReduceTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartReduceTestButtonClick()"); + resetData(); + subscription = startReduceOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_reduce_test) - public void onStartReduceTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartReduceTestButtonClick()"); - resetData(); - subscription = startReduceOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_reduce_with_global_seed_test) + public void onStartReduceOperatorTestWithGlobalSeedButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartReduceOperatorTestWithGlobalSeedButtonClick()"); + resetData(); + startReduceOperatorTestWithGlobalSeed(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_reduce_with_global_seed_test) - public void onStartReduceOperatorTestWithGlobalSeedButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartReduceOperatorTestWithGlobalSeedButtonClick()"); - resetData(); - startReduceOperatorTestWithGlobalSeed(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_reduce_with_null_seed_test) + public void onStartReduceOperatorTestWithNullSeedButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartReduceOperatorTestWithNullSeedButtonClick()"); + resetData(); + startReduceOperatorTestWithNullSeed(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_start_reduce_with_null_seed_test) - public void onStartReduceOperatorTestWithNullSeedButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartReduceOperatorTestWithNullSeedButtonClick()"); - resetData(); - startReduceOperatorTestWithNullSeed(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } + private Observable emitItems(Integer numberOfItems, boolean randomItems) { + Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); - private Observable emitItems(Integer numberOfItems, boolean randomItems) { - Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + return Observable - return Observable + // Emit N items based on the "numberOfItems" parameter + .range(1, numberOfItems) - // Emit N items based on the "numberOfItems" parameter - .range(1, numberOfItems) + .map((item) -> randomItems ? (new Random().nextInt(20 - 0) + 0) : item) - .map((item) -> randomItems ? (new Random().nextInt(20 - 0) + 0) : item ) + .doOnNext((number) -> { + try { + Log.v(TAG, "emitItems() - Emitting number: " + number); - .doOnNext((number) -> { - try { - Log.v(TAG, "emitItems() - Emitting number: " + number); + // Sleep for sometime between 100 and 300 milliseconds + Thread.sleep(new Random().nextInt(300 - 100) + 100); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - // Sleep for sometime between 100 and 300 milliseconds - Thread.sleep(new Random().nextInt(300 - 100) + 100); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }).doOnCompleted(() -> Log.v(TAG, "onCompleted")); + } - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } + private Subscription startSumOperatorTest() { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - .doOnCompleted(() -> Log.v(TAG, "onCompleted")); - } + return MathObservable.sumInteger(emitItems(20, true)) + // Just for log purpose + .compose(showDebugMessages("sumInteger")) - private Subscription startSumOperatorTest() { + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - return MathObservable.sumInteger(emitItems(20, true)) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Just for log purpose - .compose(showDebugMessages("sumInteger")) + private Subscription startMaxOperatorTest() { - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + return MathObservable.max(emitItems(20, true)) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - private Subscription startMaxOperatorTest() { + // Just for log purpose + .compose(showDebugMessages("max")) - return MathObservable.max(emitItems(20, true)) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Just for log purpose - .compose(showDebugMessages("max")) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + /** + * This example adds emitted items to the stateFactory (i.e.: an array of Integer). In the end, it acts like the toList() operator. + */ + private Subscription startCollectTestWithEmptyStateFactory() { - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + Func0> stateFactory = () -> new ArrayList<>(); + return emitItems(5, false) - /** - * This example adds emitted items to the stateFactory (i.e.: an array of Integer). In the end, it acts like the toList() operator. - * - * @return - */ - private Subscription startCollectTestWithEmptyStateFactory() { + .collect(stateFactory, (list, item) -> list.add(item)) - Func0< ArrayList> stateFactory = () -> new ArrayList<>(); + // Just for log purpose + .compose(showDebugMessages("collect")) - return emitItems(5, false) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - .collect(stateFactory, (list, item) -> list.add(item)) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Just for log purpose - .compose(showDebugMessages("collect")) + /** + * The only difference from the previous example is that on this one, we initialize stateFactory with some values. + */ + private Subscription startCollectTestWithStateFactoryWithValues() { - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + Func0> stateFactory = () -> { + ArrayList list = new ArrayList<>(); + list.add(44); + list.add(55); + return list; + }; - /** - * The only difference from the previous example is that on this one, we initialize stateFactory with some values. - * - * @return - */ - private Subscription startCollectTestWithStateFactoryWithValues() { + return emitItems(5, false) - Func0< ArrayList> stateFactory = () -> { - ArrayList list = new ArrayList<>(); - list.add(44); - list.add(55); - return list; - }; + .collect(stateFactory, (list, item) -> list.add(item)) - return emitItems(5, false) + // Just for log purpose + .compose(showDebugMessages("collect")) - .collect(stateFactory, (list, item) -> list.add(item)) - - // Just for log purpose - .compose(showDebugMessages("collect")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * This example will return the max emitted value by using reduce() operator. - * - * @return - */ - private Subscription startReduceOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItems(15, true) + /** + * This example will return the max emitted value by using reduce() operator. + */ + private Subscription startReduceOperatorTest() { - // Reduce operator will only emit onNext when source observable terminates. - .reduce((accumulator, item) -> { - Integer max = item > accumulator ? item : accumulator; - Log.v(TAG, "reduce() - item: " + item + " - current accumulator: " + accumulator + " - new accumulator (max): " + max); - return max; - }) - - // Just for log purpose - .compose(showDebugMessages("reduce")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * This example is intended to demonstrate reduce operator usage with a seed that is shared between all subscription. - * This will impact in the items and result might not be what we expect. - * - * See link below (item #8) for a very good explanation about it: - * - * http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations_14.html - * - * Also see this link: - * - * https://stackoverflow.com/questions/30633799/can-rxjava-reduce-be-unsafe-when-parallelized - * - */ - private void startReduceOperatorTestWithGlobalSeed() { - - // Emit only tree numbers - Observable> observable = emitItems(3, false) - - // Note this example will share [new ArrayList()] between all evaluations of the chain. So, the result might not be what we expect. - // The next example shows how to fix it. Thanks to Dávid Karnok (http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations_14.html) - .reduce(new ArrayList(), (accumulator, item) -> { - Log.v(TAG, "reduce() - item: " + item + " - accumulator: " + accumulator); - accumulator.add(item); - return accumulator; - }) - - // Just for log purpose - .compose(showDebugMessages("reduce")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()); - - - // Now, subscribe it for the first time... - Log.v(TAG, "Subscribe for the fist time..."); - observable.subscribe(resultSubscriber(tvResult)); - - // ... and for the second time... - Log.v(TAG, "Subscribe for the second time..."); - observable.subscribe(resultSubscriber(tvResult)); - - // ... and for the third time - Log.v(TAG, "Subscribe for the third time..."); - observable.subscribe(resultSubscriber(tvResult)); - } - - /** - * This example is intended to demonstrate reduce operator with a seed that is NOT shared between all evaluation - * of the chain. - * - * See link below (item #8) for a very good explanation about it: - * - * http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations_14.html - * - */ - private void startReduceOperatorTestWithNullSeed() { - - // Emit only tree items - Observable> observable = emitItems(3, false) - - // Reduce operator will only emit onNext when source observable terminates. - .reduce((ArrayList)null, (accumulator, item) -> { - if (accumulator == null) { - Log.v(TAG, "reduce() - accumulator is NULL. Instantiate it. This appears to be the first time this method is called."); - accumulator = new ArrayList<>(); - } - - Log.v(TAG, "reduce() - item: " + item + " - accumulator: " + accumulator); - accumulator.add(item); - return accumulator; - }) - - // Just for log purpose - .compose(showDebugMessages("reduce")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()); - - - // Now, subscribe it for the first time... - Log.v(TAG, "Subscribe for the fist time..."); - observable.subscribe(resultSubscriber(tvResult)); - - // ... and for the second time... - Log.v(TAG, "Subscribe for the second time..."); - observable.subscribe(resultSubscriber(tvResult)); - - // ... and for the third time - Log.v(TAG, "Subscribe for the third time..."); - observable.subscribe(resultSubscriber(tvResult)); - } + return emitItems(15, true) + + // Reduce operator will only emit onNext when source observable terminates. + .reduce((accumulator, item) -> { + Integer max = item > accumulator ? item : accumulator; + Log.v(TAG, "reduce() - item: " + + item + + " - current accumulator: " + + accumulator + + " - new accumulator (max): " + + max); + return max; + }) + + // Just for log purpose + .compose(showDebugMessages("reduce")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * This example is intended to demonstrate reduce operator usage with a seed that is shared between all subscription. + * This will impact in the items and result might not be what we expect. + * + * See link below (item #8) for a very good explanation about it: + * + * http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations_14.html + * + * Also see this link: + * + * https://stackoverflow.com/questions/30633799/can-rxjava-reduce-be-unsafe-when-parallelized + */ + private void startReduceOperatorTestWithGlobalSeed() { + + // Emit only tree numbers + Observable> observable = emitItems(3, false) + + // Note this example will share [new ArrayList()] between all evaluations of the chain. So, the result might not be what we expect. + // The next example shows how to fix it. Thanks to Dávid Karnok (http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations_14.html) + .reduce(new ArrayList(), (accumulator, item) -> { + Log.v(TAG, "reduce() - item: " + item + " - accumulator: " + accumulator); + accumulator.add(item); + return accumulator; + }) + + // Just for log purpose + .compose(showDebugMessages("reduce")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()); + + // Now, subscribe it for the first time... + Log.v(TAG, "Subscribe for the fist time..."); + observable.subscribe(resultSubscriber(tvResult)); + + // ... and for the second time... + Log.v(TAG, "Subscribe for the second time..."); + observable.subscribe(resultSubscriber(tvResult)); + + // ... and for the third time + Log.v(TAG, "Subscribe for the third time..."); + observable.subscribe(resultSubscriber(tvResult)); + } + + /** + * This example is intended to demonstrate reduce operator with a seed that is NOT shared between all evaluation + * of the chain. + * + * See link below (item #8) for a very good explanation about it: + * + * http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations_14.html + */ + private void startReduceOperatorTestWithNullSeed() { + + // Emit only tree items + Observable> observable = emitItems(3, false) + + // Reduce operator will only emit onNext when source observable terminates. + .reduce((ArrayList) null, (accumulator, item) -> { + if (accumulator == null) { + Log.v(TAG, + "reduce() - accumulator is NULL. Instantiate it. This appears to be the first time this method is called."); + accumulator = new ArrayList<>(); + } + + Log.v(TAG, "reduce() - item: " + item + " - accumulator: " + accumulator); + accumulator.add(item); + return accumulator; + }) + + // Just for log purpose + .compose(showDebugMessages("reduce")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()); + + // Now, subscribe it for the first time... + Log.v(TAG, "Subscribe for the fist time..."); + observable.subscribe(resultSubscriber(tvResult)); + + // ... and for the second time... + Log.v(TAG, "Subscribe for the second time..."); + observable.subscribe(resultSubscriber(tvResult)); + + // ... and for the third time + Log.v(TAG, "Subscribe for the third time..."); + observable.subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/CombiningObservablesExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/CombiningObservablesExampleActivity.java index 110809e..c9c0f97 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/CombiningObservablesExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/CombiningObservablesExampleActivity.java @@ -5,18 +5,15 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -24,479 +21,456 @@ public class CombiningObservablesExampleActivity extends BaseActivity { - private static final String TAG = CombiningObservablesExampleActivity.class.getSimpleName(); - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; - - private List oddNumbers = new ArrayList(Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15)); - private List evenNumbers = new ArrayList(Arrays.asList(2, 4, 6, 8, 10, 12, 14)); - private List someNumbers = new ArrayList(Arrays.asList(100, 200, 300, 400, 500, 600, 700)); - private List someMoreNumbers = new ArrayList(Arrays.asList(1000, 2000, 3000, 4000, 5000, 6000, 7000)); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_combining_observables_example); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + private static final String TAG = CombiningObservablesExampleActivity.class.getSimpleName(); + + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; + + private List oddNumbers = new ArrayList(Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15)); + private List evenNumbers = new ArrayList(Arrays.asList(2, 4, 6, 8, 10, 12, 14)); + private List someNumbers = + new ArrayList(Arrays.asList(100, 200, 300, 400, 500, 600, 700)); + private List someMoreNumbers = + new ArrayList(Arrays.asList(1000, 2000, 3000, 4000, 5000, 6000, 7000)); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_combining_observables_example); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_merge_operator_using_a_list_of_observables_test) + public void onStartMergeOperatorUsingListOfObservablesTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartMergeOperatorUsingListOfObservablesTestButtonClick()"); + resetData(); + subscription = startMergeOperatorUsingListOfObservablesTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_merge_operator_using_a_list_of_observables_test) - public void onStartMergeOperatorUsingListOfObservablesTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartMergeOperatorUsingListOfObservablesTestButtonClick()"); - resetData(); - subscription = startMergeOperatorUsingListOfObservablesTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_merge_operator_using_multiples_observables_test) + public void onStartMergeOperatorUsingMultiplesObservablesTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartMergeOperatorUsingMultiplesObservablesTestButtonClick()"); + resetData(); + subscription = startMergeOperatorUsingMultiplesObservablesTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_merge_operator_using_multiples_observables_test) - public void onStartMergeOperatorUsingMultiplesObservablesTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartMergeOperatorUsingMultiplesObservablesTestButtonClick()"); - resetData(); - subscription = startMergeOperatorUsingMultiplesObservablesTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_merge_operator_using_multiples_observables_and_emit_an_error_test) + public void onStartMergeOperatorUsingMultiplesObservablesAndEmitAnErrorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartMergeOperatorUsingMultiplesObservablesAndEmitAnErrorTestButtonClick()"); + resetData(); + subscription = startMergeOperatorUsingMultiplesObservablesAndEmitsAnErrorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_merge_operator_using_multiples_observables_and_emit_an_error_test) - public void onStartMergeOperatorUsingMultiplesObservablesAndEmitAnErrorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartMergeOperatorUsingMultiplesObservablesAndEmitAnErrorTestButtonClick()"); - resetData(); - subscription = startMergeOperatorUsingMultiplesObservablesAndEmitsAnErrorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_merge_delay_error__operator_test) + public void onStartMergeDelayErrorOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartMergeDelayErrorOperatorTestButtonClick()"); + resetData(); + subscription = startMergeDelayErrorOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_merge_delay_error__operator_test) - public void onStartMergeDelayErrorOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartMergeDelayErrorOperatorTestButtonClick()"); - resetData(); - subscription = startMergeDelayErrorOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_mergewith_operator_test) + public void onStartMergeWithOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartMergeWithOperatorTestButtonClick()"); + resetData(); + subscription = startMergeWithOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_mergewith_operator_test) - public void onStartMergeWithOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartMergeWithOperatorTestButtonClick()"); - resetData(); - subscription = startMergeWithOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_zip_operator_test) + public void onStartZipOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartZipOperatorTestButtonClick()"); + resetData(); + subscription = startZipOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_zip_operator_test) - public void onStartZipOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartZipOperatorTestButtonClick()"); - resetData(); - subscription = startZipOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_zipwith_operator_test) + public void onStartZipWithOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartZipWithOperatorTestButtonClick()"); + resetData(); + startZipWithOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_zipwith_operator_test) - public void onStartZipWithOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartZipWithOperatorTestButtonClick()"); - resetData(); - startZipWithOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_combinelatest_operator_test) + public void onStartCombineLatestOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartCombineLatestOperatorTestButtonClick()"); + resetData(); + startCombineLatestOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_combinelatest_operator_test) - public void onStartCombineLatestOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartCombineLatestOperatorTestButtonClick()"); - resetData(); - startCombineLatestOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_switchonnext_operator_test) + public void onStartSwitchOnNextOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartSwitchOnNextOperatorTestButtonClick()"); + resetData(); + startSwitchOnNextOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_switchonnext_operator_test) - public void onStartSwitchOnNextOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartSwitchOnNextOperatorTestButtonClick()"); - resetData(); - startSwitchOnNextOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_switchonnext_operator_test2) + public void onStartSwitchOnNextOperatorTest2ButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartSwitchOnNextOperatorTest2ButtonClick()"); + resetData(); + startSwitchOnNextOperatorTest2(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } + + private Observable emitNumbers(List list, final Integer timeToSleep) { + Log.v(TAG, "emitNumbers() - timeToSleep: " + timeToSleep); + + return Observable - @OnClick(R.id.btn_switchonnext_operator_test2) - public void onStartSwitchOnNextOperatorTest2ButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartSwitchOnNextOperatorTest2ButtonClick()"); - resetData(); - startSwitchOnNextOperatorTest2(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } + // Use timer will make this observable emits items in a separate thread. + .timer(0, TimeUnit.SECONDS) - private Observable emitNumbers(List list, final Integer timeToSleep) { - Log.v(TAG, "emitNumbers() - timeToSleep: " + timeToSleep); + .flatMap((tick) -> Observable.from(list).doOnNext((number) -> { + try { + Thread.sleep(timeToSleep); + Log.v(TAG, "emitNumbers() - Emitting number: " + number); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - return Observable + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + })); + } - // Use timer will make this observable emits items in a separate thread. - .timer(0, TimeUnit.SECONDS) + /** + * Helper method that emits an error after 1 second + */ + private Observable emitError() { + Log.v(TAG, "emitError()"); - .flatMap((tick) -> - Observable - .from(list) - .doOnNext((number) -> { - try { - Thread.sleep(timeToSleep); - Log.v(TAG, "emitNumbers() - Emitting number: " + number); + return Observable + // Use timer will make this observable emits items in a separate thread. + .timer(0, TimeUnit.SECONDS) - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } + .error(new Throwable()).doOnError(o -> { + try { + Log.v(TAG, "emitError() - Sleeping 1 second before emit an error"); + Thread.sleep(1000); + Log.v(TAG, "emitError() - *** Emitting an error ***"); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " error")); + }); + } - }) - ); - } + /** + * This example will combine the output of two observables by using merge() operator. + * + * For the downstream operator, it will look like items were emitted by a single observable. + */ + private Subscription startMergeOperatorUsingListOfObservablesTest() { - /** - * Helper method that emits an error after 1 second - * - * @return - */ - private Observable emitError() { - Log.v(TAG, "emitError()"); - - return Observable - // Use timer will make this observable emits items in a separate thread. - .timer(0, TimeUnit.SECONDS) - - .error(new Throwable()) - .doOnError(o -> { - try { - Log.v(TAG, "emitError() - Sleeping 1 second before emit an error"); - Thread.sleep(1000); - Log.v(TAG, "emitError() - *** Emitting an error ***"); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " error")); - }); - } - - /** - * This example will combine the output of two observables by using merge() operator. - * - * For the downstream operator, it will look like items were emitted by a single observable. - * - * @return - */ - private Subscription startMergeOperatorUsingListOfObservablesTest() { - - List> list = Arrays.asList(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150)); - - return Observable - - // This demonstrate we can pass a list of observables instead of multiples observables (up to nine) to the merge operator - .merge(list) - - // Just for log purpose - .compose(showDebugMessages("merge")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * This is another merge variant. It accepts multiple observables (up to nine). - * - * @return - */ - private Subscription startMergeOperatorUsingMultiplesObservablesTest() { - - return Observable - - // This demonstrate we can pass multiple observables (up to nine) to the merge operator - .merge(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150), emitNumbers(someNumbers, 700),emitNumbers(someMoreNumbers, 70)) + List> list = + Arrays.asList(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150)); - // Just for log purpose - .compose(showDebugMessages("merge")) + return Observable - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // This demonstrate we can pass a list of observables instead of multiples observables (up to nine) to the merge operator + .merge(list) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * This example shows what happen when any individual observable passed into merge terminates with an error. This makes the whole chain - * to terminate immediately with an error. - * - * @return - */ - private Subscription startMergeOperatorUsingMultiplesObservablesAndEmitsAnErrorTest() { + // Just for log purpose + .compose(showDebugMessages("merge")) - return Observable + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // This demonstrate we can pass multiple observables (up to nine) to the merge operator - .merge(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150), emitError()) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Just for log purpose - .compose(showDebugMessages("merge")) + /** + * This is another merge variant. It accepts multiple observables (up to nine). + */ + private Subscription startMergeOperatorUsingMultiplesObservablesTest() { - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + return Observable - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // This demonstrate we can pass multiple observables (up to nine) to the merge operator + .merge(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150), + emitNumbers(someNumbers, 700), emitNumbers(someMoreNumbers, 70)) - /** - * Quite similar to the previous example, but in case of error, it won't terminate - * immediately, but process all emitted items from the sources, and only then it will terminate with an error - * - * @return - */ - private Subscription startMergeDelayErrorOperatorTest() { + // Just for log purpose + .compose(showDebugMessages("merge")) - return Observable + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Even when emitError() emits an error, mergeDelayError will keep processing source emissions and only then it will emit an error - .mergeDelayError(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150), emitError()) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Just for log purpose - .compose(showDebugMessages("mergeDelayError")) + /** + * This example shows what happen when any individual observable passed into merge terminates with an error. This makes the whole chain + * to terminate immediately with an error. + */ + private Subscription startMergeOperatorUsingMultiplesObservablesAndEmitsAnErrorTest() { - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + return Observable - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // This demonstrate we can pass multiple observables (up to nine) to the merge operator + .merge(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150), emitError()) - /** - * This example is pretty similar to the merge operator. But this way we can merge sequences one by one in a chain. - * - * @return - */ - private Subscription startMergeWithOperatorTest() { + // Just for log purpose + .compose(showDebugMessages("merge")) - return emitNumbers(oddNumbers, 250) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // merge with this sequence... - .mergeWith(emitNumbers(evenNumbers, 150)) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // ... and this one... - .mergeWith(emitNumbers(someNumbers, 700)) + /** + * Quite similar to the previous example, but in case of error, it won't terminate + * immediately, but process all emitted items from the sources, and only then it will terminate with an error + */ + private Subscription startMergeDelayErrorOperatorTest() { - // ... and this one - .mergeWith(emitNumbers(someMoreNumbers, 70)) + return Observable - // Just for log purpose - .compose(showDebugMessages("mergeWith")) + // Even when emitError() emits an error, mergeDelayError will keep processing source emissions and only then it will emit an error + .mergeDelayError(emitNumbers(oddNumbers, 250), emitNumbers(evenNumbers, 150), emitError()) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("mergeDelayError")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * The zip operator combines emitted values from observables (from two up to nine) based on their index. - * - * This example will combine two observables (one that emits even numbers and another emitting odd numbers) by emitting a list of two numbers - * Ex: [1,2], [3,4], [5,6], etc. - * - * Note: The zip sequence will terminate when any of the source sequences terminates. Further values from the other sequences will be ignored. - * - * @return - */ - private Subscription startZipOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return Observable + /** + * This example is pretty similar to the merge operator. But this way we can merge sequences one by one in a chain. + */ + private Subscription startMergeWithOperatorTest() { - .zip(emitNumbers(oddNumbers, 100), emitNumbers(evenNumbers, 150), (odd, even) -> Arrays.asList(odd, even)) + return emitNumbers(oddNumbers, 250) - // Just for log purpose - .compose(showDebugMessages("zip")) + // merge with this sequence... + .mergeWith(emitNumbers(evenNumbers, 150)) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // ... and this one... + .mergeWith(emitNumbers(someNumbers, 700)) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // ... and this one + .mergeWith(emitNumbers(someMoreNumbers, 70)) - /** - * There is also a zip variation called zipWith. It allows us to combine the result of zip operators in chain - * - * @return - */ - private Subscription startZipWithOperatorTest() { + // Just for log purpose + .compose(showDebugMessages("mergeWith")) - return emitNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7), 100) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - .zipWith(emitNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7), 150), (i1, i2) -> i1 + i2) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - .zipWith(emitNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7), 550), (sumOf_i1_plus_i2, i3) -> sumOf_i1_plus_i2 + i3) + /** + * The zip operator combines emitted values from observables (from two up to nine) based on their index. + * + * This example will combine two observables (one that emits even numbers and another emitting odd numbers) by emitting a list of two numbers + * Ex: [1,2], [3,4], [5,6], etc. + * + * Note: The zip sequence will terminate when any of the source sequences terminates. Further values from the other sequences will be ignored. + */ + private Subscription startZipOperatorTest() { + + return Observable + + .zip(emitNumbers(oddNumbers, 100), emitNumbers(evenNumbers, 150), + (odd, even) -> Arrays.asList(odd, even)) + + // Just for log purpose + .compose(showDebugMessages("zip")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * There is also a zip variation called zipWith. It allows us to combine the result of zip operators in chain + */ + private Subscription startZipWithOperatorTest() { + + return emitNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7), 100) + + .zipWith(emitNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7), 150), (i1, i2) -> i1 + i2) + + .zipWith(emitNumbers(Arrays.asList(1, 2, 3, 4, 5, 6, 7), 550), + (sumOf_i1_plus_i2, i3) -> sumOf_i1_plus_i2 + i3) + + // Just for log purpose + .compose(showDebugMessages("zipWith")) + + .observeOn(AndroidSchedulers.mainThread()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * From the docs: + * + * "When any of the source Observables emits an item, CombineLatest combines the most recently emitted items from each of the other + * source Observables, using a function you provide, and emits the return value from that function." + * + * That being said, this is what this example will output: + * + * 1 -> this is emitted at time 1000ms by the first observable + * 2 -> this is emitted at time 1500ms by the second observable + * [1,2] -> This is what will be combined, based on the provided function, since these numbers were the last item emitted by each observable + * 3 -> this is emitted at time 2000ms by the first observable + * [3,2] -> This is what will be combined, (number 3 just emitted by the first observable and number 2 that is the latest item emitted by the second observable) + * 4 -> this is emitted at time 3000ms by the second observable + * [3,4] -> This is what will be combined, (number 3 emitted by the first observable and number 4 that was just emitted by the second observable) + * 5 -> this is emitted at time 3000ms by the first observable (note this was emitted at the same time as the latest item emitted by the second observable + * [5,4] -> This is what will be combined + */ + private Subscription startCombineLatestOperatorTest() { + + return Observable + + .combineLatest(emitNumbers(oddNumbers, 1000), emitNumbers(evenNumbers, 1500), + (odd, even) -> Arrays.asList(odd, even)) + + // Just for log purpose + .compose(showDebugMessages("combineLatest")) + + .observeOn(AndroidSchedulers.mainThread()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * The switchOnNext operator takes an observable that emits observable. When the outer observable emits a new item, the inner + * observable will be terminated and started again. + * + * So, our outer observable will emit numbers every 600ms. Then, for each item it emits, our inner observable will emit items + * each 180ms (giving it a chance to emit 3 items). When the outer observable emits a new item, the inner observable will be + * discarded and a new one will be used to emit items. + * + * So the expected result is a sequence of numbers 0, 1 and 2 (that were emitted from the inner Observables) + */ + private Subscription startSwitchOnNextOperatorTest() { + + return Observable + + // This is the outer observable. Items emitted here will be used to control the inner observable. Whenever it emits + // an item, the inner observable will stop its emission and a new one will be created. + .switchOnNext(Observable.interval(600, TimeUnit.MILLISECONDS) + + // We are using doOnNext here in order to be able to update our GUI. + .doOnNext((number) -> { + Log.v(TAG, "outer observable - Emitting number: " + number); + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }).map((aLong) -> { + // This is the inner observable. It will emit items every 180ms. When the outer observable emits a new item + // (which is supposed to happen after 600ms) this one will be discarded and a new one will be taken in place. + // Since outer observable will emit items each 600ms, inner observable will have a chance to emit 3 items and + // then be discarded. + return Observable.interval(180, TimeUnit.MILLISECONDS) + + // We are using doOnNext here in order to be able to update our GUI. + .doOnNext((number) -> { + Log.v(TAG, "inner observable - Emitting number: " + number); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule( + () -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }); + })) - // Just for log purpose - .compose(showDebugMessages("zipWith")) + // Just adding some boundaries + .take(15) - .observeOn(AndroidSchedulers.mainThread()) + .map((aLong) -> aLong.intValue()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Just for log purpose + .compose(showDebugMessages("switchOnNext")) - /** - * From the docs: - * - * "When any of the source Observables emits an item, CombineLatest combines the most recently emitted items from each of the other - * source Observables, using a function you provide, and emits the return value from that function." - * - * That being said, this is what this example will output: - * - * 1 -> this is emitted at time 1000ms by the first observable - * 2 -> this is emitted at time 1500ms by the second observable - * [1,2] -> This is what will be combined, based on the provided function, since these numbers were the last item emitted by each observable - * 3 -> this is emitted at time 2000ms by the first observable - * [3,2] -> This is what will be combined, (number 3 just emitted by the first observable and number 2 that is the latest item emitted by the second observable) - * 4 -> this is emitted at time 3000ms by the second observable - * [3,4] -> This is what will be combined, (number 3 emitted by the first observable and number 4 that was just emitted by the second observable) - * 5 -> this is emitted at time 3000ms by the first observable (note this was emitted at the same time as the latest item emitted by the second observable - * [5,4] -> This is what will be combined - * - * @return - */ - private Subscription startCombineLatestOperatorTest() { - - return Observable - - .combineLatest( - emitNumbers(oddNumbers, 1000), emitNumbers(evenNumbers, 1500), (odd, even) -> Arrays.asList(odd, even)) - - // Just for log purpose - .compose(showDebugMessages("combineLatest")) - - .observeOn(AndroidSchedulers.mainThread()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + .observeOn(AndroidSchedulers.mainThread()) - /** - * The switchOnNext operator takes an observable that emits observable. When the outer observable emits a new item, the inner - * observable will be terminated and started again. - * - * So, our outer observable will emit numbers every 600ms. Then, for each item it emits, our inner observable will emit items - * each 180ms (giving it a chance to emit 3 items). When the outer observable emits a new item, the inner observable will be - * discarded and a new one will be used to emit items. - * - * So the expected result is a sequence of numbers 0, 1 and 2 (that were emitted from the inner Observables) - * - * @return - */ - private Subscription startSwitchOnNextOperatorTest() { - - return Observable - - // This is the outer observable. Items emitted here will be used to control the inner observable. Whenever it emits - // an item, the inner observable will stop its emission and a new one will be created. - .switchOnNext(Observable.interval(600, TimeUnit.MILLISECONDS) - - // We are using doOnNext here in order to be able to update our GUI. - .doOnNext((number) -> { - Log.v(TAG, "outer observable - Emitting number: " + number); - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }) - .map((aLong) -> { - // This is the inner observable. It will emit items every 180ms. When the outer observable emits a new item - // (which is supposed to happen after 600ms) this one will be discarded and a new one will be taken in place. - // Since outer observable will emit items each 600ms, inner observable will have a chance to emit 3 items and - // then be discarded. - return Observable.interval(180, TimeUnit.MILLISECONDS) - - // We are using doOnNext here in order to be able to update our GUI. - .doOnNext((number) -> { - Log.v(TAG, "inner observable - Emitting number: " + number); - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }); - }) - ) - - // Just adding some boundaries - .take(15) - - .map((aLong) -> aLong.intValue()) - - // Just for log purpose - .compose(showDebugMessages("switchOnNext")) - - .observeOn(AndroidSchedulers.mainThread()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - private void startSwitchOnNextOperatorTest2() { + private void startSwitchOnNextOperatorTest2() { - Observable - .switchOnNext(Observable.timer(0, TimeUnit.MILLISECONDS) - .map(num -> emitNumbers(oddNumbers, 180)) + Observable.switchOnNext( + Observable.timer(0, TimeUnit.MILLISECONDS).map(num -> emitNumbers(oddNumbers, 180)) - .concatWith(Observable.timer(1000, TimeUnit.MILLISECONDS) - .map(num2 -> emitNumbers(evenNumbers, 500))) - ) + .concatWith(Observable.timer(1000, TimeUnit.MILLISECONDS) + .map(num2 -> emitNumbers(evenNumbers, 500)))) - // Just for log purpose - .compose(showDebugMessages("switchOnNext")) + // Just for log purpose + .compose(showDebugMessages("switchOnNext")) - .observeOn(AndroidSchedulers.mainThread()) + .observeOn(AndroidSchedulers.mainThread()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConcatMapAndFlatMapExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConcatMapAndFlatMapExampleActivity.java index 906b215..3de1d7c 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConcatMapAndFlatMapExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConcatMapAndFlatMapExampleActivity.java @@ -5,15 +5,12 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - -import com.motondon.rxjavademoapp.R; -import com.motondon.rxjavademoapp.view.base.BaseActivity; - -import java.util.concurrent.TimeUnit; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; +import com.motondon.rxjavademoapp.R; +import com.motondon.rxjavademoapp.view.base.BaseActivity; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -25,148 +22,141 @@ * http://fernandocejas.com/2015/01/11/rxjava-observable-tranformation-concatmap-vs-flatmap/ * * Please, visit it in order to get more details about it. - * */ public class ConcatMapAndFlatMapExampleActivity extends BaseActivity { - private static final String TAG = ConcatMapAndFlatMapExampleActivity.class.getSimpleName(); - - @BindView(R.id.tv_original_emitted_items) TextView originalEmittedItems; - @BindView(R.id.tv_flat_map_result) TextView flatMapResult; - @BindView(R.id.tv_concat_map_result) TextView concatMapResult; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_concatmap_flatmap_example); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + private static final String TAG = ConcatMapAndFlatMapExampleActivity.class.getSimpleName(); + + @BindView(R.id.tv_original_emitted_items) + TextView originalEmittedItems; + @BindView(R.id.tv_flat_map_result) + TextView flatMapResult; + @BindView(R.id.tv_concat_map_result) + TextView concatMapResult; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_concatmap_flatmap_example); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - @OnClick(R.id.btn_flatmap_test) - public void onFlatMapTestButtonClick() { - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onFlatMapTestButtonClick"); - originalEmittedItems.setText(""); - flatMapResult.setText(""); - subscription = flatMapTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_flatmap_test) + public void onFlatMapTestButtonClick() { + + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onFlatMapTestButtonClick"); + originalEmittedItems.setText(""); + flatMapResult.setText(""); + subscription = flatMapTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_concatmap_test) - public void onConcatMapTestButtonClick() { - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onConcatMapTestButtonClick"); - originalEmittedItems.setText(""); - concatMapResult.setText(""); - subscription = concatMapTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_concatmap_test) + public void onConcatMapTestButtonClick() { + + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onConcatMapTestButtonClick"); + originalEmittedItems.setText(""); + concatMapResult.setText(""); + subscription = concatMapTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - private Observable emitData() { + private Observable emitData() { - return Observable - .range(1, 10) - .doOnNext((number) -> { - try { - Log.v(TAG, "emitData() - Emitting number: " + number); - Thread.sleep(100); + return Observable.range(1, 10).doOnNext((number) -> { + try { + Log.v(TAG, "emitData() - Emitting number: " + number); + Thread.sleep(100); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> originalEmittedItems.setText(originalEmittedItems.getText() + " " + number)); + }); + } - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> originalEmittedItems.setText(originalEmittedItems.getText() + " " + number)); - }); - } - - /** - * This is a very simple test just to demonstrate how flatMap operator works. - * - * Basically (and according to the documentation), FlatMap merges the emissions of these Observables, so that they may interleave. - * - * @return - */ - private Subscription flatMapTest() { + /** + * This is a very simple test just to demonstrate how flatMap operator works. + * + * Basically (and according to the documentation), FlatMap merges the emissions of these Observables, so that they may interleave. + */ + private Subscription flatMapTest() { - return emitData() + return emitData() - .flatMap((data) -> - Observable + .flatMap((data) -> Observable - .just(data) + .just(data) - .compose(showDebugMessages("just")) + .compose(showDebugMessages("just")) - // Just adding a delay here, so that we can better see elements being emitted in the GUI - .delay(200, TimeUnit.MILLISECONDS) + // Just adding a delay here, so that we can better see elements being emitted in the GUI + .delay(200, TimeUnit.MILLISECONDS) - .compose(showDebugMessages("delay")) - ) + .compose(showDebugMessages("delay"))) - // Just for log purpose - .compose(showDebugMessages("flatMap")) + // Just for log purpose + .compose(showDebugMessages("flatMap")) - .map((data) -> data.toString()) + .map((data) -> data.toString()) - // Just for log purpose - .compose(showDebugMessages("map")) + // Just for log purpose + .compose(showDebugMessages("map")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(flatMapResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(flatMapResult)); + } - /** - * This example is similar to the flatMapTest, but as the name implies, it uses concatMap operator instead. - * - * Note that concatMap() uses concat operator so that it cares about the order of the emitted elements. - * - * @return - */ - private Subscription concatMapTest() { + /** + * This example is similar to the flatMapTest, but as the name implies, it uses concatMap operator instead. + * + * Note that concatMap() uses concat operator so that it cares about the order of the emitted elements. + */ + private Subscription concatMapTest() { - return emitData() + return emitData() - .concatMap((data) -> { - // Here we added some log messages allowing us to analyse the concatMap() operator behavior. - // We can see in the log messages that concatMap emits its items as they are received (after applies its function) - return Observable + .concatMap((data) -> { + // Here we added some log messages allowing us to analyse the concatMap() operator behavior. + // We can see in the log messages that concatMap emits its items as they are received (after applies its function) + return Observable - .just(data) + .just(data) - .compose(showDebugMessages("just")) + .compose(showDebugMessages("just")) - // Just adding a delay here, so that we can better see elements being emitted in the GUI - .delay(200, TimeUnit.MILLISECONDS) + // Just adding a delay here, so that we can better see elements being emitted in the GUI + .delay(200, TimeUnit.MILLISECONDS) - .compose(showDebugMessages("delay")); - }) + .compose(showDebugMessages("delay")); + }) - // Just for log purpose - .compose(showDebugMessages("concatMap")) + // Just for log purpose + .compose(showDebugMessages("concatMap")) - .map((data) -> data.toString()) + .map((data) -> data.toString()) - // Just for log purpose - .compose(showDebugMessages("map")) + // Just for log purpose + .compose(showDebugMessages("map")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(concatMapResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(concatMapResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConditionalOperatorsExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConditionalOperatorsExampleActivity.java index 398eaac..83e1fa3 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConditionalOperatorsExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ConditionalOperatorsExampleActivity.java @@ -6,18 +6,15 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -25,412 +22,409 @@ public class ConditionalOperatorsExampleActivity extends BaseActivity { - private static final String TAG = ConditionalOperatorsExampleActivity.class.getSimpleName(); + private static final String TAG = ConditionalOperatorsExampleActivity.class.getSimpleName(); - private static final int MAGIC_NUMBER = 7; + private static final int MAGIC_NUMBER = 7; - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_conditional_operators_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_conditional_operators_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_start_skipwhile_test) + public void onStartSkipWhileTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartSkipWhileTestButtonClick()"); + resetData(); + subscription = startSkipWhileTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_skipwhile_test) - public void onStartSkipWhileTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartSkipWhileTestButtonClick()"); - resetData(); - subscription = startSkipWhileTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_skipuntil_test) + public void onStartSkipUntilTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartSkiUntilTestButtonClick()"); + resetData(); + + new AlertDialog.Builder(this).setTitle("SkipUntil Test") + .setMessage( + "Emitted items will be skipped until a fake operation finishes its job (which can take up to 5 seconds)") + .setPositiveButton("OK", (dialog, which) -> subscription = startSkipUntilTest()) + .show(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_skipuntil_test) - public void onStartSkipUntilTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartSkiUntilTestButtonClick()"); - resetData(); - - new AlertDialog.Builder(this) - .setTitle("SkipUntil Test") - .setMessage("Emitted items will be skipped until a fake operation finishes its job (which can take up to 5 seconds)") - .setPositiveButton("OK", (dialog, which) -> subscription = startSkipUntilTest()) - .show(); - - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_takewhile_test) + public void onStartTakeWhileTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTakeWhileTestButtonClick()"); + resetData(); + subscription = startTakeWhileTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_takewhile_test) - public void onStartTakeWhileTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTakeWhileTestButtonClick()"); - resetData(); - subscription = startTakeWhileTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } - - @OnClick(R.id.btn_start_takeuntil_test) - public void onStartTakeUntilTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTakeUntilTestButtonClick()"); - resetData(); - - new AlertDialog.Builder(this) - .setTitle("TakeUntil Test") - .setMessage("Emitted items will be emitted downstream only while a fake operation is being executed (which can take up to 5 seconds)") - .setPositiveButton("OK", (dialog, which) -> subscription = startTakeUntilTest()) - .show(); - - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_takeuntil_test) + public void onStartTakeUntilTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTakeUntilTestButtonClick()"); + resetData(); + + new AlertDialog.Builder(this).setTitle("TakeUntil Test") + .setMessage( + "Emitted items will be emitted downstream only while a fake operation is being executed (which can take up to 5 seconds)") + .setPositiveButton("OK", (dialog, which) -> subscription = startTakeUntilTest()) + .show(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_amb_operator_test) - public void onStartAmbOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartAmbOperatorButtonClick()"); - resetData(); - subscription = startAmbOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_amb_operator_test) + public void onStartAmbOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartAmbOperatorButtonClick()"); + resetData(); + subscription = startAmbOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_all_operator_test) - public void onStartAllOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartAllOperatorButtonClick()"); - resetData(); - subscription = startAllOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_all_operator_test) + public void onStartAllOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartAllOperatorButtonClick()"); + resetData(); + subscription = startAllOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_contains_operator_test) - public void onStartContainsOperatorTest() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartContainsOperatorTest()"); - resetData(); - subscription = startContainsOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_contains_operator_test) + public void onStartContainsOperatorTest() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartContainsOperatorTest()"); + resetData(); + subscription = startContainsOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_exists_operator_test) - public void onStartExistsOperatorTest() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartExistsOperatorTest()"); - resetData(); - subscription = startExistsOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_exists_operator_test) + public void onStartExistsOperatorTest() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartExistsOperatorTest()"); + resetData(); + subscription = startExistsOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } + + /** + * Helper method that emits N random numbers + */ + private Observable emitItems(Integer numberOfItems, boolean randomSleep) { + Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + + return Observable + + // Emit N items based on the "numberOfItems" parameter + .range(0, numberOfItems) + + // Generate a random number (for each emitted item) + .map((randomNumber) -> new Random().nextInt(10)) + + .doOnNext((n) -> { + + try { + int timeToSleep = 400; + if (randomSleep) { + timeToSleep = new Random().nextInt(1000 - 200) + 200; + } + Log.v(TAG, "emitItems() - Sleeping " + timeToSleep + " before emit number " + n); + Thread.sleep(timeToSleep); + Log.v(TAG, "emitItems() - Emitting number: " + n); + } catch (InterruptedException e) { + Log.v(TAG, "emitItems() - Got an InterruptedException!"); + } + + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); + }); + } + + /** + * Helper method that emits a list of numbers by using a random delay + */ + private Observable emitItems(List list) { + Log.v(TAG, "emitOddNumbers()"); + + int timeToSleep = new Random().nextInt(1000 - 200) + 200; + + return Observable.zip(Observable.from(list), + Observable.interval(timeToSleep, TimeUnit.MILLISECONDS), (a, b) -> a).doOnNext((n) -> { + Log.v(TAG, "emitItems() - Emitting number: " + n); + + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); + }); + } + + private Observable doSomeContinuouslyOperation() { + Log.v(TAG, "doSomeContinuouslyOperation()"); + + return Observable.interval(100, TimeUnit.MILLISECONDS) + .map((number) -> number.intValue()) + .doOnNext((number) -> { + try { + runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Starting operation", + Toast.LENGTH_SHORT).show()); + + Integer timeToSleep = new Random().nextInt(6 - 3) + 3; + Log.v(TAG, + "doSomeContinuouslyOperation() - Sleeping for " + timeToSleep + " second(s)"); + Thread.sleep(timeToSleep * 1000); + + runOnUiThread( + () -> Toast.makeText(getApplicationContext(), "Operation done", Toast.LENGTH_SHORT) + .show()); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }) + .take(1); + } + + /** + * This example will emit 30random numbers (between 0 and 10) and skip them until it gets number 7. Once it gets it, it will stop skip them (in other words: + * start propagate them downstream). + */ + private Subscription startSkipWhileTest() { + + return emitItems(30, false) + + .skipWhile((number) -> { + boolean shouldSkip; + + if (number == MAGIC_NUMBER) { + // When we get our magic number (i.e. number seven) we will stop skipping. + Log.v(TAG, + "skipWhile() - Hey, we got number " + MAGIC_NUMBER + ". Lets stop skipping!"); + shouldSkip = false; + } else { + // Skip while random number is different from our MAGIC_NUMBER (number seven) + Log.v(TAG, "skipWhile() - Got number: " + + number + + ". Skipping while we do not get number seven"); + shouldSkip = true; + } + + return shouldSkip; + }) + + // This should be printed only after skipWhile receives the number seven (our MAGIC_NUMBER). + .doOnNext((item) -> Log.v(TAG, "skipWhile() - OnNext(" + item + ")")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + private Subscription startSkipUntilTest() { + + return emitItems(30, false) + + .skipUntil((doSomeContinuouslyOperation())) + + // We will hit here after doSomeContinuouslyOperation method returns, since it will emit an observable making skipUntil stop skipping. - /** - * Helper method that emits N random numbers - * - * @param numberOfItems - * @return - */ - private Observable emitItems(Integer numberOfItems, boolean randomSleep) { - Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - return Observable + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Emit N items based on the "numberOfItems" parameter - .range(0, numberOfItems) + /** + * Note that this test will be finished (i.e.: subscriber::onCompleted will be called) when we get number SEVEN, since takeWhile will emit onComplete making + * interval() operator to also finish its job. + */ + private Subscription startTakeWhileTest() { - // Generate a random number (for each emitted item) - .map((randomNumber) -> new Random().nextInt(10)) + return emitItems(30, false) - .doOnNext((n) -> { + .takeWhile((number) -> { + boolean shouldTake; - try { - int timeToSleep = 400; - if (randomSleep) { - timeToSleep = new Random().nextInt(1000 - 200) + 200; + if (number == MAGIC_NUMBER) { + // When we get our magic number (i.e. number seven) it will stop take items and emit onCompleted instead of onNext. + Log.v(TAG, + "takeWhile() - Hey, we got number " + MAGIC_NUMBER + ". Our job is done here."); + shouldTake = false; + } else { + // Take emitted items (i.e.: emit them downstream) while they are different from our MAGIC_NUMBER (number seven) + Log.v(TAG, "takeWhile() - Got number: " + + number + + ". Emit it while we do not get number SEVEN"); + shouldTake = true; + } - } - Log.v(TAG, "emitItems() - Sleeping " + timeToSleep + " before emit number " + n); - Thread.sleep(timeToSleep); - Log.v(TAG, "emitItems() - Emitting number: " + n); + return shouldTake; + }) - } catch (InterruptedException e) { - Log.v(TAG, "emitItems() - Got an InterruptedException!"); - } + // This will be printed while takeWhile gets numbers different from seven + .doOnNext((number) -> Log.v(TAG, "takeWhile() - doOnNext(" + number + ")")) - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); - }); - } + // When takeWhile receives number seven, it will complete. + .doOnCompleted(() -> Log.v(TAG, "takeWhile() - doOnCompleted().")) - /** - * Helper method that emits a list of numbers by using a random delay - * - * @param list - * @return - */ - private Observable emitItems(List list) { - Log.v(TAG, "emitOddNumbers()"); - - int timeToSleep = new Random().nextInt(1000 - 200) + 200; - - return Observable.zip( - Observable.from(list), - Observable.interval(timeToSleep, TimeUnit.MILLISECONDS), - (a, b) -> a ) - .doOnNext((n) -> { - Log.v(TAG, "emitItems() - Emitting number: " + n); - - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); - }); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - private Observable doSomeContinuouslyOperation() { - Log.v(TAG, "doSomeContinuouslyOperation()"); + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return Observable.interval(100, TimeUnit.MILLISECONDS) - .map((number) -> number.intValue()) - .doOnNext((number) -> { - try { - runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Starting operation", Toast.LENGTH_SHORT).show()); + private Subscription startTakeUntilTest() { + return emitItems(30, false) - Integer timeToSleep = new Random().nextInt(6 - 3) + 3; - Log.v(TAG, "doSomeContinuouslyOperation() - Sleeping for " + timeToSleep + " second(s)"); - Thread.sleep(timeToSleep * 1000); + .takeUntil((doSomeContinuouslyOperation())) - runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Operation done", Toast.LENGTH_SHORT).show()); + // We will hit here until doSomeContinuouslyOperation method is being executed. After that, it will emit an observable making takeUntil to complete. - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }).take(1); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * This example will emit 30random numbers (between 0 and 10) and skip them until it gets number 7. Once it gets it, it will stop skip them (in other words: - * start propagate them downstream). - * - * @return - */ - private Subscription startSkipWhileTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItems(30, false) + /** + * According to the docs, amb() operator can have multiple source observables, but will emit all items from ONLY the first of these Observables + * to emit an item or notification. All the others will be discarded. + */ + private Subscription startAmbOperatorTest() { - .skipWhile((number) -> { - boolean shouldSkip; + final List oddNumbers = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15); + final List evenNumbers = Arrays.asList(2, 4, 6, 8, 10, 12, 14); - if (number == MAGIC_NUMBER) { - // When we get our magic number (i.e. number seven) we will stop skipping. - Log.v(TAG, "skipWhile() - Hey, we got number " + MAGIC_NUMBER + ". Lets stop skipping!"); - shouldSkip = false; + return Observable.amb(emitItems(oddNumbers), emitItems(evenNumbers)) - } else { - // Skip while random number is different from our MAGIC_NUMBER (number seven) - Log.v(TAG, "skipWhile() - Got number: " + number + ". Skipping while we do not get number seven"); - shouldSkip = true; - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - return shouldSkip; - }) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // This should be printed only after skipWhile receives the number seven (our MAGIC_NUMBER). - .doOnNext((item) -> Log.v(TAG, "skipWhile() - OnNext(" + item + ")")) + /** + * If all emitted numbers are even, all() operator will emit true and complete, otherwise it will emit false before completes + */ + private Subscription startAllOperatorTest() { - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + return emitItems(3, false) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + .all(number -> number % 2 == 0) - private Subscription startSkipUntilTest() { + // doOnNext argument will contain a true value when all items emitted from the source are even. Otherwise it will be false. + .doOnNext((number) -> Log.v(TAG, "all() - doOnNext(" + number + ")")) - return emitItems(30, false) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - .skipUntil((doSomeContinuouslyOperation())) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // We will hit here after doSomeContinuouslyOperation method returns, since it will emit an observable making skipUntil stop skipping. + private Subscription startContainsOperatorTest() { - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + return emitItems(5, false) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * Note that this test will be finished (i.e.: subscriber::onCompleted will be called) when we get number SEVEN, since takeWhile will emit onComplete making - * interval() operator to also finish its job. - * - * @return - */ - private Subscription startTakeWhileTest() { + .contains(3) - return emitItems(30, false) + .doOnNext((n) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - .takeWhile((number) -> { - boolean shouldTake; + if (n) { + Log.v(TAG, + "startContainsOperatorTest() - contains() operator returned true, which means number three was emitted"); + w.schedule(() -> tvEmittedNumbers.setText( + tvEmittedNumbers.getText() + " Wow! Number three was emitted! ")); + } else { + Log.v(TAG, + "startContainsOperatorTest() - contains() operator returned false, meaning source observable did not emit number three "); + w.schedule(() -> tvEmittedNumbers.setText( + tvEmittedNumbers.getText() + " Source did not emit number three! ")); + } + }) - if (number == MAGIC_NUMBER) { - // When we get our magic number (i.e. number seven) it will stop take items and emit onCompleted instead of onNext. - Log.v(TAG, "takeWhile() - Hey, we got number " + MAGIC_NUMBER + ". Our job is done here."); - shouldTake = false; + .doOnCompleted(() -> Log.v(TAG, "startContainsOperatorTest() - doOnCompleted()")) - } else { - // Take emitted items (i.e.: emit them downstream) while they are different from our MAGIC_NUMBER (number seven) - Log.v(TAG, "takeWhile() - Got number: " + number + ". Emit it while we do not get number SEVEN"); - shouldTake = true; - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - return shouldTake; - }) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // This will be printed while takeWhile gets numbers different from seven - .doOnNext((number) -> Log.v(TAG, "takeWhile() - doOnNext(" + number + ")")) + private Subscription startExistsOperatorTest() { - // When takeWhile receives number seven, it will complete. - .doOnCompleted(() -> Log.v(TAG, "takeWhile() - doOnCompleted().")) + return emitItems(10, false) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + .exists((number) -> number % 3 == 0) - private Subscription startTakeUntilTest() { - return emitItems(30, false) + .doOnNext((n) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - .takeUntil((doSomeContinuouslyOperation())) + if (n) { + Log.v(TAG, + "startExistsOperatorTest() - exists() operator returned true, which means a number multiple of three was emitted"); + w.schedule(() -> tvEmittedNumbers.setText( + tvEmittedNumbers.getText() + " Wow! A number multiple of three was emitted! ")); + } else { + Log.v(TAG, + "startExistsOperatorTest() - exists() operator returned false, meaning source observable did not emit a number multiple of three "); + w.schedule(() -> tvEmittedNumbers.setText( + tvEmittedNumbers.getText() + " Source did not emit a number multiple of three! ")); + } + }) - // We will hit here until doSomeContinuouslyOperation method is being executed. After that, it will emit an observable making takeUntil to complete. - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + .doOnCompleted(() -> Log.v(TAG, "startExistsOperatorTest() - doOnCompleted()")) - /** - * According to the docs, amb() operator can have multiple source observables, but will emit all items from ONLY the first of these Observables - * to emit an item or notification. All the others will be discarded. - * - * @return - */ - private Subscription startAmbOperatorTest() { + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - final List oddNumbers = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15); - final List evenNumbers = Arrays.asList(2, 4, 6, 8, 10, 12, 14); - - return Observable.amb(emitItems(oddNumbers), emitItems(evenNumbers)) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * If all emitted numbers are even, all() operator will emit true and complete, otherwise it will emit false before completes - * - * @return - */ - private Subscription startAllOperatorTest() { - - return emitItems(3, false) - - .all(number -> number % 2 == 0) - - // doOnNext argument will contain a true value when all items emitted from the source are even. Otherwise it will be false. - .doOnNext((number) -> Log.v(TAG, "all() - doOnNext(" + number + ")")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - private Subscription startContainsOperatorTest() { - - return emitItems(5, false) - - .contains(3) - - .doOnNext((n) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - - if (n) { - Log.v(TAG, "startContainsOperatorTest() - contains() operator returned true, which means number three was emitted"); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " Wow! Number three was emitted! ")); - } else { - Log.v(TAG, "startContainsOperatorTest() - contains() operator returned false, meaning source observable did not emit number three "); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " Source did not emit number three! ")); - } - }) - - .doOnCompleted(() -> Log.v(TAG, "startContainsOperatorTest() - doOnCompleted()")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - private Subscription startExistsOperatorTest() { - - return emitItems(10, false) - - .exists((number) -> number % 3 == 0) - - .doOnNext((n) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - - if (n) { - Log.v(TAG, "startExistsOperatorTest() - exists() operator returned true, which means a number multiple of three was emitted"); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " Wow! A number multiple of three was emitted! ")); - } else { - Log.v(TAG, "startExistsOperatorTest() - exists() operator returned false, meaning source observable did not emit a number multiple of three "); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " Source did not emit a number multiple of three! ")); - } - }) - - .doOnCompleted(() -> Log.v(TAG, "startExistsOperatorTest() - doOnCompleted()")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ErrorHandlingExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ErrorHandlingExampleActivity.java index 1846b6a..9d79f10 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ErrorHandlingExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/ErrorHandlingExampleActivity.java @@ -5,18 +5,15 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -29,334 +26,327 @@ * http://reactivex.io/RxJava/javadoc/rx/Observable.html#onErrorReturn(rx.functions.Func1) * * Please, visit them in order to get into details. - * */ public class ErrorHandlingExampleActivity extends BaseActivity { - class MyException extends Exception { - public MyException(String detailMessage) { - super(detailMessage); - } + class MyException extends Exception { + public MyException(String detailMessage) { + super(detailMessage); } + } - private static final String TAG = ErrorHandlingExampleActivity.class.getSimpleName(); + private static final String TAG = ErrorHandlingExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_values) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_values) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_error_handling_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_error_handling_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_start_unchecked_exception_test) + public void onStartUncheckedExceptionTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartUncheckedExceptionTestButtonClick()"); + resetData(); + subscription = startUncheckedExceptionTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_unchecked_exception_test) - public void onStartUncheckedExceptionTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartUncheckedExceptionTestButtonClick()"); - resetData(); - subscription = startUncheckedExceptionTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_checked_exception_test) + public void onStartCheckedExceptionTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartCheckedExceptionTestButtonClick()"); + resetData(); + subscription = startCheckedExceptionTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_checked_exception_test) - public void onStartCheckedExceptionTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartCheckedExceptionTestButtonClick()"); - resetData(); - subscription = startCheckedExceptionTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_onerrorreturn_operator_test) + public void onStartOnErrorReturnOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartOnErrorReturnOperatorTestButtonClick()"); + resetData(); + subscription = startOnErrorReturnOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_onerrorreturn_operator_test) - public void onStartOnErrorReturnOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartOnErrorReturnOperatorTestButtonClick()"); - resetData(); - subscription = startOnErrorReturnOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_onerrorresumenext_operator_test) + public void onStartOnErrorResumeNextOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartOnErrorResumeNextOperatorTestButtonClick()"); + resetData(); + subscription = startOnErrorResumeNextOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_onerrorresumenext_operator_test) - public void onStartOnErrorResumeNextOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartOnErrorResumeNextOperatorTestButtonClick()"); - resetData(); - subscription = startOnErrorResumeNextOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_onexceptionresumenext_operator_test) + public void onStartOnExceptionResumeNextOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartOnExceptionResumeNextOperatorTestButtonClick()"); + resetData(); + subscription = startOnExceptionResumeNextOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_start_onexceptionresumenext_operator_test) - public void onStartOnExceptionResumeNextOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartOnExceptionResumeNextOperatorTestButtonClick()"); - resetData(); - subscription = startOnExceptionResumeNextOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } + private Observable emitRandomNumber() { + Log.v(TAG, "emitRandomNumber()"); - private Observable emitRandomNumber() { - Log.v(TAG, "emitRandomNumber()"); + return Observable.create((s) -> { + final int randomNumber = new Random().nextInt(10); - return Observable.create((s) -> { - final int randomNumber = new Random().nextInt(10); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + randomNumber)); - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + randomNumber)); + s.onNext(randomNumber); + s.onCompleted(); + }); + } - s.onNext(randomNumber); - s.onCompleted(); - }); - } - - private Observable emitSecondObservable() { - Log.v(TAG, "emitSecondObservable()"); - - final List list = Arrays.asList("@", "#", "$", "%", "&"); - - return Observable - - .from(list) - - .doOnNext((n) -> { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); - }); - } - - private String throwAnException() throws MyException { - Log.v(TAG, "throwAnException() - Throwing MyException()..."); + private Observable emitSecondObservable() { + Log.v(TAG, "emitSecondObservable()"); - throw new MyException("This is an example of a checked exception"); - } - - /** - * Unchecked exceptions will be caught by the subscriber.onError. We can see it in the logs. - * - * We could of course wrap our code to handle it, but this example is intended to demonstrate how unchecked - * exceptions are handled by the sequence. - * - * @return - */ - private Subscription startUncheckedExceptionTest() { - - return Observable.interval(1, TimeUnit.SECONDS) - - .flatMap((tick) -> emitRandomNumber()) - - .map((randomNumber) -> { - // Only forces an exception when random number is greater than seven. - if (randomNumber > 7) { - Log.v(TAG, "map() - We will throw a RuntimeException..."); - throw new RuntimeException("Hey, this is a forced runtime exception..."); - } else { - Log.v(TAG, "Returning random number: " + randomNumber); - return "" + randomNumber; - } - }) - - // Just for log purpose - .compose(showDebugMessages("map")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + final List list = Arrays.asList("@", "#", "$", "%", "&"); - /** - * Since this example catches a checked exception, we can do whatever we want. Here we will emit an error, which will terminate the - * sequence with an error - * - * @return - */ - private Subscription startCheckedExceptionTest() { - - return Observable.interval(1, TimeUnit.SECONDS) - - .flatMap((tick) -> emitRandomNumber()) - - .flatMap((randomNumber) -> { - // Only forces an exception when random number is greater than seven. - if (randomNumber > 7) { - try { - // This is just to simulate a method call which can throw a checked exception. Since checked exception must be explicit caught (in a try/catch - // block or throwing it), we catch it here and emit an error. - Log.v(TAG, "Throwing an exception. Random number: " + randomNumber); - return Observable.just(throwAnException()); - - } catch (Throwable e) { - Log.v(TAG, "Catching the exception and emitting an error..."); - // Here we catch our exception and emit an error. We could of course emit whatever value we wanted, but this is only to demonstrate - // this error will terminate the sequence and handled by the observer.onError - return Observable.error(e); - } - } else { - // If number is less than seven, just re-emit it - Log.v(TAG, "Just re-emitting random number: " + randomNumber); - return Observable.just("" + randomNumber); - } - }) - - // Just for log purpose - .compose(showDebugMessages("flatMap")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + return Observable - /** - * From the docs: - * - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, - * the Observable invokes its Observer's onError method, and then quits without invoking any more of its Observer's methods. - * The onErrorReturn method changes this behavior. If you pass a function (resumeFunction) to an Observable's onErrorReturn - * method, if the original Observable encounters an error, instead of invoking its Observer's onError method, it will - * instead emit the return value of resumeFunction. - * - * From the Dan Lew blog (mentioned on the top of this activity): - * - * "... when using these [error handling] operators, the upstream Observables are still going to shut down! They've already - * seen a terminal event (onError); all that onError[Return|ResumeNext] does is replace the onError notification with a - * different sequence downstream." ... "You might expect the interval to continue emitting after the map throws an exception, - * but it doesn't! Only the downstream subscribers avoid onError." - * - * @return - */ - private Subscription startOnErrorReturnOperatorTest() { - - return Observable.interval(1, TimeUnit.SECONDS) - - .flatMap((tick) -> emitRandomNumber()) - - .map((randomNumber) -> { - // Only forces an exception when random number is greater than seven. - if (randomNumber > 7) { - Log.v(TAG, "Forcing an exception. Random number: " + randomNumber); - return String.valueOf(4 / 0); - } else { - Log.v(TAG, "Returning random number: " + randomNumber); - return "" + randomNumber; - } - }) - - // When source observable throws an exception, it will be caught here and we will emit an string instead of the error. This is what onErrorReturn operator is for. - .onErrorReturn((error) -> " [This is a fallback string which will be emitted instead of the error emitted from the source observable (we forced an exception when source observable emitted a random number greater than 7.]") - - // Just for log purpose - .compose(showDebugMessages("onErrorReturn")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + .from(list) - /** - * From the docs: - * - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, - * the Observable invokes its Observer's onError method, and then quits without invoking any more of its Observer's methods. - * The onErrorResumeNext method changes this behavior. If you pass another Observable (resumeSequence) to an Observable's - * onErrorResumeNext method, if the original Observable encounters an error, instead of invoking its Observer's onError - * method, it will instead relinquish control to resumeSequence which will invoke the Observer's onNext method if it is - * able to do so. In such a case, because no Observable necessarily invokes onError, the Observer may never know that an - * error happened. - * - * @return - */ - private Subscription startOnErrorResumeNextOperatorTest() { - - return Observable.interval(1, TimeUnit.SECONDS) - - .flatMap((tick) -> emitRandomNumber()) - - .map((randomNumber) -> { - // Only forces an exception when random number is greater than seven. - if (randomNumber > 7) { - Log.v(TAG, "Forcing an exception. Random number: " + randomNumber); - return String.valueOf(4 / 0); - } else { - Log.v(TAG, "Returning random number: " + randomNumber); - return "" + randomNumber; - } - }) - - .onErrorResumeNext((throwable) -> { - Log.v(TAG, "onErrorResumeNext() - Found an error: " + throwable.getMessage() + " - Emitting a second Observable instead..."); - return emitSecondObservable(); - }) - - // Just for log purpose - .compose(showDebugMessages("onErrorResumeNext")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * From the docs: - * - * This differs from onErrorResumeNext(rx.functions.Func1>) in - * that this one does not handle Throwable or Error but lets those continue through. - * - * @return - */ - private Subscription startOnExceptionResumeNextOperatorTest() { - - return Observable.interval(1, TimeUnit.SECONDS) - - .flatMap((tick) -> emitRandomNumber()) - - .map((randomNumber) -> { - // Only forces an exception when random number is greater than seven. - if (randomNumber > 7) { - Log.v(TAG, "Forcing an exception. Random number: " + randomNumber); - return String.valueOf(4 / 0); - } else { - Log.v(TAG, "Returning random number: " + randomNumber); - return "" + randomNumber; - } - }) - - .onExceptionResumeNext(emitSecondObservable()) - - // Just for log purpose - .compose(showDebugMessages("onExceptionResumeNext")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + .doOnNext((n) -> { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); + }); + } + + private String throwAnException() throws MyException { + Log.v(TAG, "throwAnException() - Throwing MyException()..."); + + throw new MyException("This is an example of a checked exception"); + } + + /** + * Unchecked exceptions will be caught by the subscriber.onError. We can see it in the logs. + * + * We could of course wrap our code to handle it, but this example is intended to demonstrate how unchecked + * exceptions are handled by the sequence. + */ + private Subscription startUncheckedExceptionTest() { + + return Observable.interval(1, TimeUnit.SECONDS) + + .flatMap((tick) -> emitRandomNumber()) + + .map((randomNumber) -> { + // Only forces an exception when random number is greater than seven. + if (randomNumber > 7) { + Log.v(TAG, "map() - We will throw a RuntimeException..."); + throw new RuntimeException("Hey, this is a forced runtime exception..."); + } else { + Log.v(TAG, "Returning random number: " + randomNumber); + return "" + randomNumber; + } + }) + + // Just for log purpose + .compose(showDebugMessages("map")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * Since this example catches a checked exception, we can do whatever we want. Here we will emit an error, which will terminate the + * sequence with an error + */ + private Subscription startCheckedExceptionTest() { + + return Observable.interval(1, TimeUnit.SECONDS) + + .flatMap((tick) -> emitRandomNumber()) + + .flatMap((randomNumber) -> { + // Only forces an exception when random number is greater than seven. + if (randomNumber > 7) { + try { + // This is just to simulate a method call which can throw a checked exception. Since checked exception must be explicit caught (in a try/catch + // block or throwing it), we catch it here and emit an error. + Log.v(TAG, "Throwing an exception. Random number: " + randomNumber); + return Observable.just(throwAnException()); + } catch (Throwable e) { + Log.v(TAG, "Catching the exception and emitting an error..."); + // Here we catch our exception and emit an error. We could of course emit whatever value we wanted, but this is only to demonstrate + // this error will terminate the sequence and handled by the observer.onError + return Observable.error(e); + } + } else { + // If number is less than seven, just re-emit it + Log.v(TAG, "Just re-emitting random number: " + randomNumber); + return Observable.just("" + randomNumber); + } + }) + + // Just for log purpose + .compose(showDebugMessages("flatMap")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * From the docs: + * + * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, + * the Observable invokes its Observer's onError method, and then quits without invoking any more of its Observer's methods. + * The onErrorReturn method changes this behavior. If you pass a function (resumeFunction) to an Observable's onErrorReturn + * method, if the original Observable encounters an error, instead of invoking its Observer's onError method, it will + * instead emit the return value of resumeFunction. + * + * From the Dan Lew blog (mentioned on the top of this activity): + * + * "... when using these [error handling] operators, the upstream Observables are still going to shut down! They've already + * seen a terminal event (onError); all that onError[Return|ResumeNext] does is replace the onError notification with a + * different sequence downstream." ... "You might expect the interval to continue emitting after the map throws an exception, + * but it doesn't! Only the downstream subscribers avoid onError." + */ + private Subscription startOnErrorReturnOperatorTest() { + + return Observable.interval(1, TimeUnit.SECONDS) + + .flatMap((tick) -> emitRandomNumber()) + + .map((randomNumber) -> { + // Only forces an exception when random number is greater than seven. + if (randomNumber > 7) { + Log.v(TAG, "Forcing an exception. Random number: " + randomNumber); + return String.valueOf(4 / 0); + } else { + Log.v(TAG, "Returning random number: " + randomNumber); + return "" + randomNumber; + } + }) + + // When source observable throws an exception, it will be caught here and we will emit an string instead of the error. This is what onErrorReturn operator is for. + .onErrorReturn( + (error) -> " [This is a fallback string which will be emitted instead of the error emitted from the source observable (we forced an exception when source observable emitted a random number greater than 7.]") + + // Just for log purpose + .compose(showDebugMessages("onErrorReturn")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * From the docs: + * + * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, + * the Observable invokes its Observer's onError method, and then quits without invoking any more of its Observer's methods. + * The onErrorResumeNext method changes this behavior. If you pass another Observable (resumeSequence) to an Observable's + * onErrorResumeNext method, if the original Observable encounters an error, instead of invoking its Observer's onError + * method, it will instead relinquish control to resumeSequence which will invoke the Observer's onNext method if it is + * able to do so. In such a case, because no Observable necessarily invokes onError, the Observer may never know that an + * error happened. + */ + private Subscription startOnErrorResumeNextOperatorTest() { + + return Observable.interval(1, TimeUnit.SECONDS) + + .flatMap((tick) -> emitRandomNumber()) + + .map((randomNumber) -> { + // Only forces an exception when random number is greater than seven. + if (randomNumber > 7) { + Log.v(TAG, "Forcing an exception. Random number: " + randomNumber); + return String.valueOf(4 / 0); + } else { + Log.v(TAG, "Returning random number: " + randomNumber); + return "" + randomNumber; + } + }) + + .onErrorResumeNext((throwable) -> { + Log.v(TAG, "onErrorResumeNext() - Found an error: " + + throwable.getMessage() + + " - Emitting a second Observable instead..."); + return emitSecondObservable(); + }) + + // Just for log purpose + .compose(showDebugMessages("onErrorResumeNext")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * From the docs: + * + * This differs from onErrorResumeNext(rx.functions.Func1>) in + * that this one does not handle Throwable or Error but lets those continue through. + */ + private Subscription startOnExceptionResumeNextOperatorTest() { + + return Observable.interval(1, TimeUnit.SECONDS) + + .flatMap((tick) -> emitRandomNumber()) + + .map((randomNumber) -> { + // Only forces an exception when random number is greater than seven. + if (randomNumber > 7) { + Log.v(TAG, "Forcing an exception. Random number: " + randomNumber); + return String.valueOf(4 / 0); + } else { + Log.v(TAG, "Returning random number: " + randomNumber); + return "" + randomNumber; + } + }) + + .onExceptionResumeNext(emitSecondObservable()) + + // Just for log purpose + .compose(showDebugMessages("onExceptionResumeNext")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/FilteringExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/FilteringExampleActivity.java index 98e77ea..a5febf5 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/FilteringExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/FilteringExampleActivity.java @@ -8,468 +8,437 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.OnItemSelected; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Random; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnItemSelected; import rx.Observable; import rx.Scheduler; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; - public class FilteringExampleActivity extends BaseActivity { - /** - * This is a helper class that is used to fill the spinner up with pairs of test name and a related method name. - * - * When an item is selected from the spinner, we will extract a method name and call it by using reflection. - * - * We extended from Pair class (instead of using it directly) since we want a custom toString() method - * - */ - class SpinnerOptionAndMethodName extends Pair { - - public SpinnerOptionAndMethodName(String first, String second) { - super(first, second); - } - - @Override - public String toString() { - return (String) first; - } + /** + * This is a helper class that is used to fill the spinner up with pairs of test name and a related method name. + * + * When an item is selected from the spinner, we will extract a method name and call it by using reflection. + * + * We extended from Pair class (instead of using it directly) since we want a custom toString() method + */ + class SpinnerOptionAndMethodName extends Pair { + + public SpinnerOptionAndMethodName(String first, String second) { + super(first, second); } - private static final String TAG = FilteringExampleActivity.class.getSimpleName(); - private static final Integer DEFAULT_VALUE = 9999; - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; - @BindView(R.id.s_test_options) Spinner sTestOptions; - - // Hold the method name related to the test user chosen in the spinner control. - private String currentTestMethodName; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_filtering_example); - ButterKnife.bind(this); - - // Fill spinner view up with all available test names and their related method names. We will use reflection to call a method based on the user choice - List testOptions = new ArrayList<>(); - testOptions.add(new SpinnerOptionAndMethodName("first()", "startFirstOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("first() with predicate", "startFirstOperatorWithPredicateFunctionTest")); - testOptions.add(new SpinnerOptionAndMethodName("firstOrDefault(9999)", "startFirstOrDefaultOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("takeFirst()", "startTakeFirstOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("single()", "startSingleOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("singleOrDefault(9999)", "startSingleOrDefaultOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("elementAt(3)", "startElementAtOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("last()", "startLastOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("lastOrDefault()", "startLastOrDefaultOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("take(5)", "startTakeOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("takeLast(5)", "startTakeLastOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("filter()", "startFilterOperatorTest")); - - ArrayAdapter adapter = new ArrayAdapter<>( - this, android.R.layout.simple_spinner_item, testOptions); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - sTestOptions.setAdapter(adapter); - - // Set the default method name - currentTestMethodName = "startFirstOperatorTest"; - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + public String toString() { + return (String) first; } - - @OnItemSelected(R.id.s_test_options) - public void spinnerTestOptionsItemSelected(Spinner spinner, int position) { - SpinnerOptionAndMethodName testItem = (SpinnerOptionAndMethodName) spinner.getAdapter().getItem(position); - - currentTestMethodName = (String) testItem.second; - - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private static final String TAG = FilteringExampleActivity.class.getSimpleName(); + private static final Integer DEFAULT_VALUE = 9999; + + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; + @BindView(R.id.s_test_options) + Spinner sTestOptions; + + // Hold the method name related to the test user chosen in the spinner control. + private String currentTestMethodName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_filtering_example); + ButterKnife.bind(this); + + // Fill spinner view up with all available test names and their related method names. We will use reflection to call a method based on the user choice + List testOptions = new ArrayList<>(); + testOptions.add(new SpinnerOptionAndMethodName("first()", "startFirstOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("first() with predicate", + "startFirstOperatorWithPredicateFunctionTest")); + testOptions.add( + new SpinnerOptionAndMethodName("firstOrDefault(9999)", "startFirstOrDefaultOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("takeFirst()", "startTakeFirstOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("single()", "startSingleOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("singleOrDefault(9999)", + "startSingleOrDefaultOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("elementAt(3)", "startElementAtOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("last()", "startLastOperatorTest")); + testOptions.add( + new SpinnerOptionAndMethodName("lastOrDefault()", "startLastOrDefaultOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("take(5)", "startTakeOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("takeLast(5)", "startTakeLastOperatorTest")); + testOptions.add(new SpinnerOptionAndMethodName("filter()", "startFilterOperatorTest")); + + ArrayAdapter adapter = + new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, testOptions); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + sTestOptions.setAdapter(adapter); + + // Set the default method name + currentTestMethodName = "startFirstOperatorTest"; + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - @OnClick(R.id.btn_start_test) - public void onButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onButtonClick()"); - - tvEmittedNumbers.setText(""); - tvResult.setText(""); - - try { - - // Instantiate an object of type method that returns a method name we will invoke - Method m = this.getClass().getDeclaredMethod(currentTestMethodName); - - // Now, invoke method user selected - subscription = (Subscription) m.invoke(this); - - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnItemSelected(R.id.s_test_options) + public void spinnerTestOptionsItemSelected(Spinner spinner, int position) { + SpinnerOptionAndMethodName testItem = + (SpinnerOptionAndMethodName) spinner.getAdapter().getItem(position); + + currentTestMethodName = (String) testItem.second; + + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_start_test) + public void onButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onButtonClick()"); + + tvEmittedNumbers.setText(""); + tvResult.setText(""); + + try { + + // Instantiate an object of type method that returns a method name we will invoke + Method m = this.getClass().getDeclaredMethod(currentTestMethodName); + + // Now, invoke method user selected + subscription = (Subscription) m.invoke(this); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - /** - * Helper method that emits N random numbers - * - * @param numberOfItems - * @return - */ - private Observable emitItems(Integer numberOfItems) { - Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + /** + * Helper method that emits N random numbers + */ + private Observable emitItems(Integer numberOfItems) { + Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); - return Observable + return Observable - // Emit N items based on the "numberOfItems" parameter - .range(0, numberOfItems) + // Emit N items based on the "numberOfItems" parameter + .range(0, numberOfItems) - // Generate a random number (for each emitted item) - .map((randomNumber) -> new Random().nextInt(10)) + // Generate a random number (for each emitted item) + .map((randomNumber) -> new Random().nextInt(10)) - .doOnNext((n) -> { + .doOnNext((n) -> { - try { - Thread.sleep(100); - Log.v(TAG, "emitItems() - Emitting number: " + n); + try { + Thread.sleep(100); + Log.v(TAG, "emitItems() - Emitting number: " + n); + } catch (InterruptedException e) { + Log.v(TAG, "emitItems() - Got an InterruptedException!"); + } - } catch (InterruptedException e) { - Log.v(TAG, "emitItems() - Got an InterruptedException!"); - } + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); + }); + } - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + n)); - }); - } - - /** - * As the name suggests, this method will emit items (up to 10) or an empty observable based on an internal criteria - * - * @return - */ - private Observable emitItemsOrEmpty() { - Log.v(TAG, "emitItemsOrEmpty()"); - - // Generate a random number - Integer randomNumber = new Random().nextInt(10 - 1) + 1; + /** + * As the name suggests, this method will emit items (up to 10) or an empty observable based on an internal criteria + */ + private Observable emitItemsOrEmpty() { + Log.v(TAG, "emitItemsOrEmpty()"); - // If it is greater than 5, emit an empty observable - if (randomNumber > 5) { + // Generate a random number + Integer randomNumber = new Random().nextInt(10 - 1) + 1; - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText("Empty observable")); + // If it is greater than 5, emit an empty observable + if (randomNumber > 5) { - return Observable.empty(); - } + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText("Empty observable")); - // Otherwise, if it is less or equals to 5, call emitItems()method which will emit N random numbers - return emitItems(new Random().nextInt(10 - 1) + 1); + return Observable.empty(); } - /** - * In case of emitItemsOrEmpty() helper method emits an item, first() operator will emit it downstream, then terminate the chain. But in - * case of an empty observable is emitted, since first() operator expects at least one item to be emitted, this example will terminate - * with the following error: "Sequence contains no elements". - * - * @return - */ - private Subscription startFirstOperatorTest() { + // Otherwise, if it is less or equals to 5, call emitItems()method which will emit N random numbers + return emitItems(new Random().nextInt(10 - 1) + 1); + } - return emitItemsOrEmpty() + /** + * In case of emitItemsOrEmpty() helper method emits an item, first() operator will emit it downstream, then terminate the chain. But in + * case of an empty observable is emitted, since first() operator expects at least one item to be emitted, this example will terminate + * with the following error: "Sequence contains no elements". + */ + private Subscription startFirstOperatorTest() { - // Emit only the first item emitted by the source Observable. In case of none item emitted, - // it terminates with an error - .first() + return emitItemsOrEmpty() - // Just for log purpose - .compose(showDebugMessages("first")) + // Emit only the first item emitted by the source Observable. In case of none item emitted, + // it terminates with an error + .first() - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("first")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * This example uses a variation of first() operator, which accepts a predicate function and will complete successfully - * when that predicate is evaluated as true. - * - * @return - */ - private Subscription startFirstOperatorWithPredicateFunctionTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // This will emit 5 random numbers from 1 to 10 - return emitItems(5) + /** + * This example uses a variation of first() operator, which accepts a predicate function and will complete successfully + * when that predicate is evaluated as true. + */ + private Subscription startFirstOperatorWithPredicateFunctionTest() { - // Emit the first number emitted from the source Observable that is multiple of three - .first((number) -> number % 3 == 0) + // This will emit 5 random numbers from 1 to 10 + return emitItems(5) - // Just for log purpose - .compose(showDebugMessages("first(...)")) + // Emit the first number emitted from the source Observable that is multiple of three + .first((number) -> number % 3 == 0) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("first(...)")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * If the source Observable emits an empty observable, default value - * will be emitted. Otherwise, if it emits some value, it will be emitted downstream and terminate the chain. - * - * @return - */ - private Subscription startFirstOrDefaultOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItemsOrEmpty() + /** + * If the source Observable emits an empty observable, default value + * will be emitted. Otherwise, if it emits some value, it will be emitted downstream and terminate the chain. + */ + private Subscription startFirstOrDefaultOperatorTest() { - // In case of the source Observable finishes before emit any item, DEFAULT_VALUE will be emitted instead - .firstOrDefault(DEFAULT_VALUE) + return emitItemsOrEmpty() - // Just for log purpose - .compose(showDebugMessages("firstOrDefault(" + DEFAULT_VALUE + ")")) + // In case of the source Observable finishes before emit any item, DEFAULT_VALUE will be emitted instead + .firstOrDefault(DEFAULT_VALUE) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("firstOrDefault(" + DEFAULT_VALUE + ")")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * From the docs: - * - * The takeFirst operator behaves similarly to first, with the exception of how these operators behave when the source Observable - * emits no items that satisfy the predicate. In such a case, first will throw a NoSuchElementException while takeFirst will return - * an empty Observable (one that calls onCompleted but never calls onNext). - * - * @return - */ - private Subscription startTakeFirstOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItemsOrEmpty() + /** + * From the docs: + * + * The takeFirst operator behaves similarly to first, with the exception of how these operators behave when the source Observable + * emits no items that satisfy the predicate. In such a case, first will throw a NoSuchElementException while takeFirst will return + * an empty Observable (one that calls onCompleted but never calls onNext). + */ + private Subscription startTakeFirstOperatorTest() { - // In case of no item is emitted by the source Observable, takeFirst will return an empty observable - .takeFirst((number) -> true) + return emitItemsOrEmpty() - // Just for log purpose - .compose(showDebugMessages("takeFirst()")) + // In case of no item is emitted by the source Observable, takeFirst will return an empty observable + .takeFirst((number) -> true) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("takeFirst()")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * single() operator expects only one item to be emitted. If more than one item is emitted it will terminate with an error (Sequence - * contains too many elements). - * - * Also, if the source Observable does not emit any item before completing, it throws a NoSuchElementException - * and will terminate with error: "Sequence contains no elements". - * - * @return - */ - private Subscription startSingleOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItemsOrEmpty() + /** + * single() operator expects only one item to be emitted. If more than one item is emitted it will terminate with an error (Sequence + * contains too many elements). + * + * Also, if the source Observable does not emit any item before completing, it throws a NoSuchElementException + * and will terminate with error: "Sequence contains no elements". + */ + private Subscription startSingleOperatorTest() { - // If emitItemsOrEmpty() emits an empty observable or more than one item, single() will terminate with an error, - // otherwise it will terminate successfully - .single() + return emitItemsOrEmpty() - // Just for log purpose - .compose(showDebugMessages("single")) + // If emitItemsOrEmpty() emits an empty observable or more than one item, single() will terminate with an error, + // otherwise it will terminate successfully + .single() - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("single")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * - * singleOrDefault() operator is slightly different from the single() operator. In case of no element is emitted, - * instead of terminate with an error, it will emit the default value. - * - * @return - */ - private Subscription startSingleOrDefaultOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItemsOrEmpty() + /** + * singleOrDefault() operator is slightly different from the single() operator. In case of no element is emitted, + * instead of terminate with an error, it will emit the default value. + */ + private Subscription startSingleOrDefaultOperatorTest() { - .singleOrDefault(DEFAULT_VALUE) + return emitItemsOrEmpty() - // Just for log purpose - .compose(showDebugMessages("singleOrDefault(" + DEFAULT_VALUE + ")")) + .singleOrDefault(DEFAULT_VALUE) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("singleOrDefault(" + DEFAULT_VALUE + ")")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * elementAt(N) expects at least N items are emitted from the source observable. In case of less items are emitted, - * IndexOutOfBoundsException is thrown. - * - * @return - */ - private Subscription startElementAtOperatorTest() { + /** + * elementAt(N) expects at least N items are emitted from the source observable. In case of less items are emitted, + * IndexOutOfBoundsException is thrown. + */ + private Subscription startElementAtOperatorTest() { - return emitItemsOrEmpty() + return emitItemsOrEmpty() - .elementAt(3) + .elementAt(3) - // Just for log purpose - .compose(showDebugMessages("elementAt(3)")) + // Just for log purpose + .compose(showDebugMessages("elementAt(3)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * last() operator emits only the last emitted item by the source Observable. In case of an empty observable is emitted, since last() operator - * expects at least one item to be emitted, it will terminate with an error (NoSuchElementException) - * - * @return - */ - private Subscription startLastOperatorTest() { + /** + * last() operator emits only the last emitted item by the source Observable. In case of an empty observable is emitted, since last() operator + * expects at least one item to be emitted, it will terminate with an error (NoSuchElementException) + */ + private Subscription startLastOperatorTest() { - return emitItemsOrEmpty() + return emitItemsOrEmpty() - // Will emit the last item emitted by the source Observable - .last() + // Will emit the last item emitted by the source Observable + .last() - // Just for log purpose - .compose(showDebugMessages("last")) + // Just for log purpose + .compose(showDebugMessages("last")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * Similar to last() operation, but in case of the source Observable fails to emit any item, a default value will be emitted instead of an error. - * - * @return - */ - private Subscription startLastOrDefaultOperatorTest() { + /** + * Similar to last() operation, but in case of the source Observable fails to emit any item, a default value will be emitted instead of an error. + */ + private Subscription startLastOrDefaultOperatorTest() { - return emitItemsOrEmpty() + return emitItemsOrEmpty() - .lastOrDefault(DEFAULT_VALUE) + .lastOrDefault(DEFAULT_VALUE) - // Just for log purpose - .compose(showDebugMessages("lastOrDefault(" + DEFAULT_VALUE + ")")) + // Just for log purpose + .compose(showDebugMessages("lastOrDefault(" + DEFAULT_VALUE + ")")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * take(N) operator returns N items emitted by the source Observable. - * - * @return - */ - private Subscription startTakeOperatorTest() { + /** + * take(N) operator returns N items emitted by the source Observable. + */ + private Subscription startTakeOperatorTest() { - return emitItemsOrEmpty() + return emitItemsOrEmpty() - // Even in case of fewer items than N are emitted (or no item is emitted), take(5) will complete after source observable completes - .take(5) + // Even in case of fewer items than N are emitted (or no item is emitted), take(5) will complete after source observable completes + .take(5) - // Just for log purpose - .compose(showDebugMessages("take(5)")) + // Just for log purpose + .compose(showDebugMessages("take(5)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * Emits the last N item emitted by the source Observable. In case of no item is emitted, it will completes with no error. - * - * @return - */ - private Subscription startTakeLastOperatorTest() { + /** + * Emits the last N item emitted by the source Observable. In case of no item is emitted, it will completes with no error. + */ + private Subscription startTakeLastOperatorTest() { - return emitItemsOrEmpty() + return emitItemsOrEmpty() - // Will emit only the last 2 items. In case of no item is emitted, it will successfully complete after source observable completes - .takeLast(5) + // Will emit only the last 2 items. In case of no item is emitted, it will successfully complete after source observable completes + .takeLast(5) - // Just for log purpose - .compose(showDebugMessages("takeLast(5)")) + // Just for log purpose + .compose(showDebugMessages("takeLast(5)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * Emits only those numbers that make predicate function to evaluate as true. - * - * @return - */ - private Subscription startFilterOperatorTest() { + /** + * Emits only those numbers that make predicate function to evaluate as true. + */ + private Subscription startFilterOperatorTest() { - return emitItems(50) + return emitItems(50) - // Will emit all the numbers emitted by the source Observable that are multiple of three - .filter((number) -> number % 3 == 0) + // Will emit all the numbers emitted by the source Observable that are multiple of three + .filter((number) -> number % 3 == 0) - // Just for log purpose - .compose(showDebugMessages("filter(...)")) + // Just for log purpose + .compose(showDebugMessages("filter(...)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/JoinExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/JoinExampleActivity.java index 4b3c8eb..1e9471f 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/JoinExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/JoinExampleActivity.java @@ -7,16 +7,13 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Arrays; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -25,186 +22,195 @@ /** * This activity allows users to test different values for join operator: - * - left Observable emission delay - * - right Observable emission delay - * - left window duration - * - right window duration - * - left Observable number of items to emit - * - right Observable number of items to emit - * + * - left Observable emission delay + * - right Observable emission delay + * - left window duration + * - right window duration + * - left Observable number of items to emit + * - right Observable number of items to emit */ public class JoinExampleActivity extends BaseActivity { - private static final String TAG = JoinExampleActivity.class.getSimpleName(); - private static final Integer NEVER_CLOSE = -1; - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; - - @BindView(R.id.et_left_observable_delay) EditText etLeftObservableDelayBetweenEmission; - @BindView(R.id.et_right_observable_delay) EditText etRightObservableDelayBetweenEmission; - @BindView(R.id.et_left_window_duration) EditText etLeftWindowDuration; - @BindView(R.id.et_right_window_duration) EditText etRightWindowDuration; - @BindView(R.id.et_left_observable_number_of_items_to_be_emitted) EditText etLeftObservableNumberOfItemsToBeEmitted; - @BindView(R.id.et_righ_observable_number_of_items_to_be_emitted) EditText etRightObservableNumberOfItemsToBeEmitted; - - private Integer leftDelayBetweenEmission; - private Integer rightDelayBetweenEmission; - private Integer leftWindowDuration; - private Integer rightWindowDuration; - private Integer leftNumberOfItemsToBeEmitted; - private Integer rightNumberOfItemsToBeEmitted; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_join_example); - ButterKnife.bind(this); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - // Prevent keyboard to be visible when activity resumes. - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + private static final String TAG = JoinExampleActivity.class.getSimpleName(); + private static final Integer NEVER_CLOSE = -1; + + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; + + @BindView(R.id.et_left_observable_delay) + EditText etLeftObservableDelayBetweenEmission; + @BindView(R.id.et_right_observable_delay) + EditText etRightObservableDelayBetweenEmission; + @BindView(R.id.et_left_window_duration) + EditText etLeftWindowDuration; + @BindView(R.id.et_right_window_duration) + EditText etRightWindowDuration; + @BindView(R.id.et_left_observable_number_of_items_to_be_emitted) + EditText etLeftObservableNumberOfItemsToBeEmitted; + @BindView(R.id.et_righ_observable_number_of_items_to_be_emitted) + EditText etRightObservableNumberOfItemsToBeEmitted; + + private Integer leftDelayBetweenEmission; + private Integer rightDelayBetweenEmission; + private Integer leftWindowDuration; + private Integer rightWindowDuration; + private Integer leftNumberOfItemsToBeEmitted; + private Integer rightNumberOfItemsToBeEmitted; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_join_example); + ButterKnife.bind(this); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + // Prevent keyboard to be visible when activity resumes. + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_join_operator_test) + public void onSstartJoinOperatorTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onSstartJoinOperatorTestButtonClick()"); + resetData(); + readData(); + subscription = startJoinOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_join_operator_test) - public void onSstartJoinOperatorTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onSstartJoinOperatorTestButtonClick()"); - resetData(); - readData(); - subscription = startJoinOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + @OnClick(R.id.btn_stop_test) + public void onStopSubscription() { + if (subscription != null) { + subscription.unsubscribe(); } - - @OnClick(R.id.btn_stop_test) - public void onStopSubscription() { - if (subscription != null) { - subscription.unsubscribe(); - } + } + + private void readData() { + leftDelayBetweenEmission = + Integer.parseInt(etLeftObservableDelayBetweenEmission.getText().toString()); + if (leftDelayBetweenEmission < 0) { + leftDelayBetweenEmission = 0; + } else if (leftDelayBetweenEmission > 5000) { + leftDelayBetweenEmission = 5000; } - - private void readData() { - leftDelayBetweenEmission = Integer.parseInt(etLeftObservableDelayBetweenEmission.getText().toString()); - if (leftDelayBetweenEmission < 0) { - leftDelayBetweenEmission = 0; - } else if (leftDelayBetweenEmission > 5000) { - leftDelayBetweenEmission = 5000; - } - Log.v(TAG, "readData() - leftDelayBetweenEmission: " + leftDelayBetweenEmission); - - rightDelayBetweenEmission = Integer.parseInt(etRightObservableDelayBetweenEmission.getText().toString()); - if (rightDelayBetweenEmission < 0) { - rightDelayBetweenEmission = 0; - } else if (rightDelayBetweenEmission > 5000) { - rightDelayBetweenEmission = 5000; - } - Log.v(TAG, "readData() - rightDelayBetweenEmission: " + rightDelayBetweenEmission); - - if (etLeftWindowDuration.getText().toString().isEmpty()) { - leftWindowDuration = NEVER_CLOSE; - } else { - leftWindowDuration = Integer.parseInt(etLeftWindowDuration.getText().toString()); - if (leftWindowDuration < 0) { - leftWindowDuration = 0; - } else if (leftWindowDuration > 5000) { - leftWindowDuration = 5000; - } - } - Log.v(TAG, "readData() - leftWindowDuration: " + leftWindowDuration); - - if (etRightWindowDuration.getText().toString().isEmpty()) { - rightWindowDuration = NEVER_CLOSE; - } else { - rightWindowDuration = Integer.parseInt(etRightWindowDuration.getText().toString()); - if (rightWindowDuration < 0) { - rightWindowDuration = 0; - } else if (rightWindowDuration > 5000) { - rightWindowDuration = 5000; - } - } - Log.v(TAG, "readData() - rightWindowDuration: " + rightWindowDuration); - - leftNumberOfItemsToBeEmitted = Integer.parseInt(etLeftObservableNumberOfItemsToBeEmitted.getText().toString()); - if (leftNumberOfItemsToBeEmitted < 1) { - leftNumberOfItemsToBeEmitted = 1; - } else if (leftNumberOfItemsToBeEmitted > 40) { - leftNumberOfItemsToBeEmitted = 40; - } - Log.v(TAG, "readData() - leftNumberOfItemsToBeEmitted: " + leftNumberOfItemsToBeEmitted); - - rightNumberOfItemsToBeEmitted = Integer.parseInt(etRightObservableNumberOfItemsToBeEmitted.getText().toString()); - if (rightNumberOfItemsToBeEmitted < 1) { - rightNumberOfItemsToBeEmitted = 1; - } else if (rightNumberOfItemsToBeEmitted > 40) { - rightNumberOfItemsToBeEmitted = 40; - } - Log.v(TAG, "readData() - rightNumberOfItemsToBeEmitted: " + rightNumberOfItemsToBeEmitted); + Log.v(TAG, "readData() - leftDelayBetweenEmission: " + leftDelayBetweenEmission); + + rightDelayBetweenEmission = + Integer.parseInt(etRightObservableDelayBetweenEmission.getText().toString()); + if (rightDelayBetweenEmission < 0) { + rightDelayBetweenEmission = 0; + } else if (rightDelayBetweenEmission > 5000) { + rightDelayBetweenEmission = 5000; } - - private Observable emitItems(Integer numberOfItemsToBeEmitted, Integer delayBetweenEmission, String caption) { - return Observable.interval(delayBetweenEmission, TimeUnit.MILLISECONDS) - .map((number) -> number.intValue()) - .doOnNext((number) -> { - Log.v(TAG, "emitItems() - " + caption + " Observable. Emitting number: " + number); - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - - }) - .take(numberOfItemsToBeEmitted) - .subscribeOn(Schedulers.newThread()); + Log.v(TAG, "readData() - rightDelayBetweenEmission: " + rightDelayBetweenEmission); + + if (etLeftWindowDuration.getText().toString().isEmpty()) { + leftWindowDuration = NEVER_CLOSE; + } else { + leftWindowDuration = Integer.parseInt(etLeftWindowDuration.getText().toString()); + if (leftWindowDuration < 0) { + leftWindowDuration = 0; + } else if (leftWindowDuration > 5000) { + leftWindowDuration = 5000; + } } - - - private Subscription startJoinOperatorTest() { - - Observable left = emitItems(leftNumberOfItemsToBeEmitted, leftDelayBetweenEmission, "left"); - Observable right = emitItems(rightNumberOfItemsToBeEmitted, rightDelayBetweenEmission, "right"); - - return left - .join(right, - i -> { - if (leftWindowDuration == NEVER_CLOSE) { - return Observable.never(); - } else { - return Observable.timer(leftWindowDuration, TimeUnit.MILLISECONDS).compose(showDebugMessages("leftDuration")).subscribeOn(Schedulers.computation()); - } - }, - i -> { - if (rightWindowDuration == NEVER_CLOSE) { - return Observable.never(); - } else { - return Observable.timer(rightWindowDuration, TimeUnit.MILLISECONDS).compose(showDebugMessages("rightDuration")).subscribeOn(Schedulers.computation()); - } - }, - (l, r) -> { - Log.v(TAG, "join() - Joining left number: " + l + " with right number: " + r); - return Arrays.asList(l.intValue(), r.intValue()); - } - ) - .compose(applySchedulers()) - .subscribe(resultSubscriber(tvResult)); + Log.v(TAG, "readData() - leftWindowDuration: " + leftWindowDuration); + + if (etRightWindowDuration.getText().toString().isEmpty()) { + rightWindowDuration = NEVER_CLOSE; + } else { + rightWindowDuration = Integer.parseInt(etRightWindowDuration.getText().toString()); + if (rightWindowDuration < 0) { + rightWindowDuration = 0; + } else if (rightWindowDuration > 5000) { + rightWindowDuration = 5000; + } } - - /** - * THIS VERSION IS THE SAME AS THE ABOVE, BUT WITH NO GUI FEEDBACK FOR THE EMISSIONS NOR COMPUTATION SCHEDULER ON THE DURATION SELECTORS. - * - * We created this version since the version above, due the GUI updates, we cant sleep (while window's are opened) nor update the GUI on the main - * thread, otherwise the main screen will freeze. The downside is that due to the different schedulers we are using the emissions might be out - * of order, making it hard to understand. So, depends on what you want: a GUI feedback or analyse log messages, comment one and uncomment - * the other. - * - **/ + Log.v(TAG, "readData() - rightWindowDuration: " + rightWindowDuration); + + leftNumberOfItemsToBeEmitted = + Integer.parseInt(etLeftObservableNumberOfItemsToBeEmitted.getText().toString()); + if (leftNumberOfItemsToBeEmitted < 1) { + leftNumberOfItemsToBeEmitted = 1; + } else if (leftNumberOfItemsToBeEmitted > 40) { + leftNumberOfItemsToBeEmitted = 40; + } + Log.v(TAG, "readData() - leftNumberOfItemsToBeEmitted: " + leftNumberOfItemsToBeEmitted); + + rightNumberOfItemsToBeEmitted = + Integer.parseInt(etRightObservableNumberOfItemsToBeEmitted.getText().toString()); + if (rightNumberOfItemsToBeEmitted < 1) { + rightNumberOfItemsToBeEmitted = 1; + } else if (rightNumberOfItemsToBeEmitted > 40) { + rightNumberOfItemsToBeEmitted = 40; + } + Log.v(TAG, "readData() - rightNumberOfItemsToBeEmitted: " + rightNumberOfItemsToBeEmitted); + } + + private Observable emitItems(Integer numberOfItemsToBeEmitted, + Integer delayBetweenEmission, String caption) { + return Observable.interval(delayBetweenEmission, TimeUnit.MILLISECONDS) + .map((number) -> number.intValue()) + .doOnNext((number) -> { + Log.v(TAG, "emitItems() - " + caption + " Observable. Emitting number: " + number); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }) + .take(numberOfItemsToBeEmitted) + .subscribeOn(Schedulers.newThread()); + } + + private Subscription startJoinOperatorTest() { + + Observable left = + emitItems(leftNumberOfItemsToBeEmitted, leftDelayBetweenEmission, "left"); + Observable right = + emitItems(rightNumberOfItemsToBeEmitted, rightDelayBetweenEmission, "right"); + + return left.join(right, i -> { + if (leftWindowDuration == NEVER_CLOSE) { + return Observable.never(); + } else { + return Observable.timer(leftWindowDuration, TimeUnit.MILLISECONDS) + .compose(showDebugMessages("leftDuration")) + .subscribeOn(Schedulers.computation()); + } + }, i -> { + if (rightWindowDuration == NEVER_CLOSE) { + return Observable.never(); + } else { + return Observable.timer(rightWindowDuration, TimeUnit.MILLISECONDS) + .compose(showDebugMessages("rightDuration")) + .subscribeOn(Schedulers.computation()); + } + }, (l, r) -> { + Log.v(TAG, "join() - Joining left number: " + l + " with right number: " + r); + return Arrays.asList(l.intValue(), r.intValue()); + }).compose(applySchedulers()).subscribe(resultSubscriber(tvResult)); + } + + /** + * THIS VERSION IS THE SAME AS THE ABOVE, BUT WITH NO GUI FEEDBACK FOR THE EMISSIONS NOR COMPUTATION SCHEDULER ON THE DURATION SELECTORS. + * + * We created this version since the version above, due the GUI updates, we cant sleep (while window's are opened) nor update the GUI on the main + * thread, otherwise the main screen will freeze. The downside is that due to the different schedulers we are using the emissions might be out + * of order, making it hard to understand. So, depends on what you want: a GUI feedback or analyse log messages, comment one and uncomment + * the other. + * + **/ /*private Subscription startJoinOperatorTest() { Observable left = Observable.interval(leftDelayBetweenEmission, TimeUnit.MILLISECONDS).take(leftNumberOfItemsToBeEmitted ); diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/MoreFilteringOperatorsExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/MoreFilteringOperatorsExampleActivity.java index 6c427ba..08b2314 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/MoreFilteringOperatorsExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/MoreFilteringOperatorsExampleActivity.java @@ -5,16 +5,13 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -22,298 +19,284 @@ public class MoreFilteringOperatorsExampleActivity extends BaseActivity { - private static final String TAG = MoreFilteringOperatorsExampleActivity.class.getSimpleName(); + private static final String TAG = MoreFilteringOperatorsExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_more_filtering_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_more_filtering_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_sample_operator_example) + public void onSampleOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onSampleOperatorButtonClick()"); + resetData(); + subscription = startSampleOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT) + .show(); } - - @OnClick(R.id.btn_sample_operator_example) - public void onSampleOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onSampleOperatorButtonClick()"); - resetData(); - subscription = startSampleOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_sample_operator_with_observable_example) + public void onSampleOperatorWithObservableButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onSampleOperatorWithObservableButtonClick()"); + resetData(); + subscription = startSampleOperatorWithObservableTest(); + } else { + Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT) + .show(); } - - @OnClick(R.id.btn_sample_operator_with_observable_example) - public void onSampleOperatorWithObservableButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onSampleOperatorWithObservableButtonClick()"); - resetData(); - subscription = startSampleOperatorWithObservableTest(); - } else { - Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_throttle_last_example) + public void onThrottleLastOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onThrottleLastOperatorButtonClick()"); + resetData(); + subscription = startThrottleLastOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT) + .show(); } - - @OnClick(R.id.btn_throttle_last_example) - public void onThrottleLastOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onThrottleLastOperatorButtonClick()"); - resetData(); - subscription = startThrottleLastOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_throttle_first_test) + public void onThrottleFirstOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onThrottleFirstOperatorButtonClick()"); + resetData(); + subscription = startThrottleFirstOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT) + .show(); } - - @OnClick(R.id.btn_throttle_first_test) - public void onThrottleFirstOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onThrottleFirstOperatorButtonClick()"); - resetData(); - subscription = startThrottleFirstOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_debounce_operator_test) + public void onDebounceOperatorButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onDebounceOperatorButtonClick()"); + resetData(); + subscription = startDebounceOperatorTest(); + } else { + Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT) + .show(); } - - @OnClick(R.id.btn_debounce_operator_test) - public void onDebounceOperatorButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onDebounceOperatorButtonClick()"); - resetData(); - subscription = startDebounceOperatorTest(); - } else { - Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_debounce_operator_with_func_test) + public void onDebounceOperatorWithFuncButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onDebounceOperatorWithFuncButtonClick()"); + resetData(); + subscription = startDebounceOperatorWithFuncTest(); + } else { + Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT) + .show(); } - - @OnClick(R.id.btn_debounce_operator_with_func_test) - public void onDebounceOperatorWithFuncButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onDebounceOperatorWithFuncButtonClick()"); - resetData(); - subscription = startDebounceOperatorWithFuncTest(); - } else { - Toast.makeText(getApplicationContext(), "A test is already running", Toast.LENGTH_SHORT).show(); + } + + /** + * Emit [numberOfItems] items either by a fixed interval (when fixedTimeToSleep is greater than zero) or by + * using a random delay (randomSleepTime) + */ + private Observable emitItems(Integer numberOfItems, Integer fixedTimeToSleep, + boolean randomSleepTime) { + Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + + return Observable.range(0, numberOfItems).doOnNext((number) -> { + try { + Integer timeToSleep = 500; + if (fixedTimeToSleep > 0) { + timeToSleep = fixedTimeToSleep; } - } - /** - * Emit [numberOfItems] items either by a fixed interval (when fixedTimeToSleep is greater than zero) or by - * using a random delay (randomSleepTime) - * - * @param numberOfItems - * @param fixedTimeToSleep - * @param randomSleepTime - * @return - */ - private Observable emitItems(Integer numberOfItems, Integer fixedTimeToSleep, boolean randomSleepTime) { - Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); - - return Observable - .range(0, numberOfItems) - .doOnNext((number) -> { - try { - Integer timeToSleep = 500; - if (fixedTimeToSleep > 0) { - timeToSleep = fixedTimeToSleep; - } - - if (randomSleepTime) { - timeToSleep = new Random().nextInt(500 - 100) + 100; - } - - Log.v(TAG, "emitItems() - Emitting number: " + number + " and sleeping for " + timeToSleep + "ms"); - Thread.sleep(timeToSleep); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - - // Now, log it on the GUI in order to inform user about the emitted item - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }); - } - - /** - * Sleep somewhere between one and five seconds just to simulate a (fake) operator - * - * @return - */ - private Observable doAFakeOperation() { - Log.v(TAG, "doAFakeOperation()"); - - return Observable.interval(100, TimeUnit.MILLISECONDS) - .map((number) -> number.intValue()) - .doOnNext((number) -> { - try { - Integer timeToSleep = new Random().nextInt(5 - 1) + 1; - Log.v(TAG, "doAFakeOperation() - Sleeping for " + timeToSleep + " second(s)"); - Thread.sleep(timeToSleep * 1000); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }).take(1); - } - - /** - * Source observable will emit 20 items, each one with 500ms of delay, but since we are using sample() operator with a - * 4 seconds of period, only the last item emitted during each period will be emitted downstream. - * - * @return - */ - private Subscription startSampleOperatorTest() { - - return emitItems(20, 500, false) - - .sample(4, TimeUnit.SECONDS) - - // Just for log purpose - .compose(showDebugMessages("sample(4s)")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * Source observable will emit some items, each one with 500ms of delay. Whenever doAFakeOperation() method terminates (somewhere between 1 and 5 seconds), - * the most recent item emitted by the emitItems() will be emitted downstream by the sample operator. - * - * @return - */ - private Subscription startSampleOperatorWithObservableTest() { - - return emitItems(50, 500, false) - - .sample(doAFakeOperation()) - - // Just for log purpose - .compose(showDebugMessages("sample(Observable)")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + if (randomSleepTime) { + timeToSleep = new Random().nextInt(500 - 100) + 100; + } - /** - * Source observable will emit some items, each one with 500ms of delay. By using throttleLast() operator with a - * 4 seconds of period, only the last item emitted on each period will be emitted. - * - * This is similar to the sample() operator - * - * @return - */ - private Subscription startThrottleLastOperatorTest() { + Log.v(TAG, + "emitItems() - Emitting number: " + number + " and sleeping for " + timeToSleep + "ms"); + Thread.sleep(timeToSleep); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + + // Now, log it on the GUI in order to inform user about the emitted item + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }); + } - return emitItems(20, 500, false) + /** + * Sleep somewhere between one and five seconds just to simulate a (fake) operator + */ + private Observable doAFakeOperation() { + Log.v(TAG, "doAFakeOperation()"); + + return Observable.interval(100, TimeUnit.MILLISECONDS) + .map((number) -> number.intValue()) + .doOnNext((number) -> { + try { + Integer timeToSleep = new Random().nextInt(5 - 1) + 1; + Log.v(TAG, "doAFakeOperation() - Sleeping for " + timeToSleep + " second(s)"); + Thread.sleep(timeToSleep * 1000); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }) + .take(1); + } - .throttleLast(4, TimeUnit.SECONDS) + /** + * Source observable will emit 20 items, each one with 500ms of delay, but since we are using sample() operator with a + * 4 seconds of period, only the last item emitted during each period will be emitted downstream. + */ + private Subscription startSampleOperatorTest() { - // Just for log purpose - .compose(showDebugMessages("throttleLast(4s)")) + return emitItems(20, 500, false) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + .sample(4, TimeUnit.SECONDS) + + // Just for log purpose + .compose(showDebugMessages("sample(4s)")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * Source observable will emit some items, each one with 500ms of delay. Whenever doAFakeOperation() method terminates (somewhere between 1 and 5 seconds), + * the most recent item emitted by the emitItems() will be emitted downstream by the sample operator. + */ + private Subscription startSampleOperatorWithObservableTest() { + + return emitItems(50, 500, false) + + .sample(doAFakeOperation()) + + // Just for log purpose + .compose(showDebugMessages("sample(Observable)")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * Source observable will emit some items, each one with 500ms of delay. By using throttleLast() operator with a + * 4 seconds of period, only the last item emitted on each period will be emitted. + * + * This is similar to the sample() operator + */ + private Subscription startThrottleLastOperatorTest() { + + return emitItems(20, 500, false) + + .throttleLast(4, TimeUnit.SECONDS) + + // Just for log purpose + .compose(showDebugMessages("throttleLast(4s)")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * Source observable will emit some items, each one with 500ms of delay. By using throttleFirst() operator with a + * 4 seconds of windowDuration, only the first item emitted during each interval will be emitted. + */ + private Subscription startThrottleFirstOperatorTest() { + + return emitItems(20, 500, false) + + .throttleFirst(4, TimeUnit.SECONDS) + + // Just for log purpose + .compose(showDebugMessages("throttleFirst(4s)")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * Source observable will emit some items, each one with 500ms of delay. By using throttleFirst() operator with a - * 4 seconds of windowDuration, only the first item emitted during each interval will be emitted. - * - * @return - */ - private Subscription startThrottleFirstOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - return emitItems(20, 500, false) + /** + * In this example, debounce uses a timeout equals to 350ms. So, it will only emit an item downstream when source Observable + * does not emit any item within 350ms. Noticed for this example, emitItems(...) method will emit items by using random interval + * between 100 and 500ms + */ + private Subscription startDebounceOperatorTest() { - .throttleFirst(4, TimeUnit.SECONDS) + // Each emission will sleep between 100 and 500 milliseconds + return Observable.timer(0, TimeUnit.SECONDS).flatMap((aLong) -> emitItems(40, 0, true)) - // Just for log purpose - .compose(showDebugMessages("throttleFirst(4s)")) + // Debounce will only emit when source observable does not emit items for 350ms + .debounce(350, TimeUnit.MILLISECONDS) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("debounce(350ms)")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * In this example, debounce uses a timeout equals to 350ms. So, it will only emit an item downstream when source Observable - * does not emit any item within 350ms. Noticed for this example, emitItems(...) method will emit items by using random interval - * between 100 and 500ms - * - * @return - */ - private Subscription startDebounceOperatorTest() { + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - // Each emission will sleep between 100 and 500 milliseconds - return Observable - .timer(0, TimeUnit.SECONDS) - .flatMap((aLong) -> emitItems(40, 0, true)) + /** + * This example will emit items each 1100ms. Debounce selector will sleep between 1 and 5 seconds. If it sleeps for 2 or more seconds, + * source Observable will emit another item while the current one is still being processed, making it to be discarded. On the other hand, if + * selector sleeps for 1 second, that means it will finish before the next emission, so it will not be dropped, but emitted downstream. + */ + private Subscription startDebounceOperatorWithFuncTest() { - // Debounce will only emit when source observable does not emit items for 350ms - .debounce(350, TimeUnit.MILLISECONDS) + return Observable.timer(0, TimeUnit.SECONDS).flatMap((aLong) -> emitItems(20, 1100, false)) - // Just for log purpose - .compose(showDebugMessages("debounce(350ms)")) + .debounce((item) -> { + Integer timeToSleep = new Random().nextInt(5 - 1) + 1; + Log.v(TAG, "startDebounceOperatorWithFuncTest() - Sleeping for " + + timeToSleep + + " second(s) while processing item " + + item); + return Observable.just(item).delay(timeToSleep, TimeUnit.SECONDS); + }) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Just for log purpose + .compose(showDebugMessages("debounce(Func1)")) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - /** - * This example will emit items each 1100ms. Debounce selector will sleep between 1 and 5 seconds. If it sleeps for 2 or more seconds, - * source Observable will emit another item while the current one is still being processed, making it to be discarded. On the other hand, if - * selector sleeps for 1 second, that means it will finish before the next emission, so it will not be dropped, but emitted downstream. - * - * @return - */ - private Subscription startDebounceOperatorWithFuncTest() { - - return Observable - .timer(0, TimeUnit.SECONDS) - .flatMap((aLong) -> emitItems(20, 1100, false)) - - .debounce((item) -> { - Integer timeToSleep = new Random().nextInt(5 - 1) + 1; - Log.v(TAG, "startDebounceOperatorWithFuncTest() - Sleeping for " + timeToSleep + " second(s) while processing item " + item); - return Observable.just(item).delay(timeToSleep, TimeUnit.SECONDS); - }) - - // Just for log purpose - .compose(showDebugMessages("debounce(Func1)")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } \ No newline at end of file diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/RetryExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/RetryExampleActivity.java index 273dac0..2cfa4c8 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/RetryExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/RetryExampleActivity.java @@ -6,18 +6,15 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -29,392 +26,357 @@ * http://blog.danlew.net/2016/01/25/rxjavas-repeatwhen-and-retrywhen-explained/ * * Please, visit it in order to get more details about it. - * */ public class RetryExampleActivity extends BaseActivity { - private static final String TAG = RetryExampleActivity.class.getSimpleName(); + private static final String TAG = RetryExampleActivity.class.getSimpleName(); - private static final int COUNTER_START = 1; - private static final int MAX_ATTEMPTS = 3; + private static final int COUNTER_START = 1; + private static final int MAX_ATTEMPTS = 3; - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - private int attemptCount = 0; + private int attemptCount = 0; - private List> retryMatrix; + private List> retryMatrix; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_retry_and_retrywhen_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_retry_and_retrywhen_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - retryMatrix = Arrays.asList( - new Pair<>( 1, 1), // First four attempts, sleep 1 second before retry - new Pair<>( 5, 2), // For attempt 5 to 9, sleep 2 second before retry - new Pair<>( 10, 3), // For attempt 10 to 19, sleep 3 second before retry - new Pair<>( 20, 4), // For attempt 20 to 39, sleep 4 second before retry - new Pair<>( 40, 5), // For attempt 40 to 99, sleep 4 second before retry - new Pair<>(100, 6) // For the 100th attempts and next ones, sleep 6 second before retry - ); - } - - private void resetData() { - attemptCount = 0; - tvEmittedNumbers.setText(""); - tvResult.setText(""); - } - - @OnClick(R.id.btn_retry_forever) - public void onRetry_ForeverButtonClick() { - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = retry_forever(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - @OnClick(R.id.btn_retry_when_forever) - public void onRetryWhen_ForeverButtonClick() { - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = retryWhen_forever(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } - - @OnClick(R.id.btn_retry_when_no_flatMap) - public void onRetryWhen_noFlatMapButtonClick() { - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = retryWhen_no_flatMap(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } - - @OnClick(R.id.btn_retry_when_three_times) - public void onRetryWhen_threeTimesButtonClick() { - - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = retryWhen_threeTimes(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + retryMatrix = + Arrays.asList(new Pair<>(1, 1), // First four attempts, sleep 1 second before retry + new Pair<>(5, 2), // For attempt 5 to 9, sleep 2 second before retry + new Pair<>(10, 3), // For attempt 10 to 19, sleep 3 second before retry + new Pair<>(20, 4), // For attempt 20 to 39, sleep 4 second before retry + new Pair<>(40, 5), // For attempt 40 to 99, sleep 4 second before retry + new Pair<>(100, 6) // For the 100th attempts and next ones, sleep 6 second before retry + ); + } + + private void resetData() { + attemptCount = 0; + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_retry_forever) + public void onRetry_ForeverButtonClick() { + + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = retry_forever(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_retry_when_three_times_with_zipwith) - public void onRetryWhen_ThreeTimesWithZipWithButtonClick() { + @OnClick(R.id.btn_retry_when_forever) + public void onRetryWhen_ForeverButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = retryWhen_threeTimes_with_zipWith(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = retryWhen_forever(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_retry_when_with_exponential_backoff) - public void onRetryWhen_WithExponentialBackoffButtonClick() { + @OnClick(R.id.btn_retry_when_no_flatMap) + public void onRetryWhen_noFlatMapButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - resetData(); - subscription = retryWhen_withExponentialBackoff(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = retryWhen_no_flatMap(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - @OnClick(R.id.btn_stop_subscription) - public void onStopSubscription() { - if (subscription != null) { - subscription.unsubscribe(); - } - } - - /** - * This method uses a random value in order to decide whether to emit either an observable or an error. - * - * Currently there is 20% of chance to emit and error and 80% of chance to emit an observable. - * - * - * @return - */ - private Observable emitItemsAndThenError() { - Log.v(TAG, "emitItemsAndThenError()"); - - return Observable - .zip( - Observable.range(0, 100), - Observable.interval(100, TimeUnit.MILLISECONDS), - (a, b) -> a) - .flatMap(n -> { - Integer random = new Random().nextInt(10 - 1) + 1; - - if (random > 2) { - Log.v(TAG, "emitItemsAndThenError() - Emitting number: " + n); - updateEmittedItemView(n.toString()); - return Observable.just(n); - } else { - try { - Thread.sleep(300); - } catch (InterruptedException e) { - Log.v(TAG, "emitItemsAndThenError() - Got an InterruptedException!"); - } - return Observable.error(new Throwable("Emitting an error")); - } - }); - } + @OnClick(R.id.btn_retry_when_three_times) + public void onRetryWhen_threeTimesButtonClick() { - private Subscription retry_forever() { - - // Remember that emitItemsAndThenError() can emit an item or an error. When it emits an error, retry will act. - return emitItemsAndThenError() - .compose(this.showDebugMessages("emitItemsAndThenError")) - .retry() - .compose(this.showDebugMessages("retry")) - .compose(applySchedulers()) - .subscribe( - value -> tvResult.setText(tvResult.getText() + " " + value), - error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), - () -> tvResult.setText(tvResult.getText() + " - onCompleted") - ); + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = retryWhen_threeTimes(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - private Subscription retryWhen_forever() { - - return emitItemsAndThenError() - .compose(this.showDebugMessages("emitItemsAndThenError")) - .retryWhen(error -> error - - // Right after retryWhen(...) subscription, it will stop here waiting for the source to emit an error. - .flatMap( error2 -> { - // Since we are always emitting source observable (the Throwable object) after timer() expires (in two seconds), this method will act - // exactly like the retry() operator. - Log.v(TAG, "retryWhen_forever::flatMap() - Emitting throwable. This will make retryWhen to re-subscribe source observable. Attempt: " + ++attemptCount); - return Observable - .timer(2, TimeUnit.SECONDS) - .compose(RetryExampleActivity.this.showDebugMessages("retryWhen.timer")); - } - ) - ) - .compose(this.showDebugMessages("retryWhen")) - .compose(applySchedulers()) - .subscribe( - value -> tvResult.setText(tvResult.getText() + " " + value), - error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), - () -> tvResult.setText(tvResult.getText() + " - onCompleted") - ); - } + @OnClick(R.id.btn_retry_when_three_times_with_zipwith) + public void onRetryWhen_ThreeTimesWithZipWithButtonClick() { - /** - * This example demonstrates how retryWhen SHOULD NOT be used. Since it does not react over the error emitted by the source, it terminates as soon as timer - * operator finishes its job (sleep for two seconds). - * - * See next three examples which uses flatMap in order to see how to use it in the right way. - * - * @return - */ - private Subscription retryWhen_no_flatMap() { - - return emitItemsAndThenError() - .compose(this.showDebugMessages("emitItemsAndThenError")) - .retryWhen((error) -> { - Log.v(TAG, "retryForever2::retryWhen()"); - - // Right after the subscription, it will get here, but since we are not reacting over the error emitted by - // the source, retryWhen() will terminate as soon as timer() operator returns (after 2 seconds) making source - // observable stops emitting and breaking the entire chain. - // - // This is actually not what we expect to see in a real application, but we added it here just to demonstrate how retryWhen actually behaves. - return error - .timer(2, TimeUnit.SECONDS) - - // Just for log purpose - .compose(showDebugMessages("retryWhen.timer")); - }) - .compose(this.showDebugMessages("retryWhen")) - .compose(applySchedulers()) - .subscribe( - value -> tvResult.setText(tvResult.getText() + " " + value), - error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), - () -> tvResult.setText(tvResult.getText() + " - onCompleted") - ); + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = retryWhen_threeTimes_with_zipWith(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - /** - * Note on this example we must explicitly cast flatmap return type to the (Observable) type in the function applied to the emitted items. - * This is because inside that function we can return multiple types of Observables (an Observable until we reach MAX_ATTEMPTS or an Observable - * after we reach MAX_ATTEMPTS value). - * - * This is needed since the compiler resolves lambda expressions types by using the first statement that uses it. In our case, since it will first find - * the line: "return Observable.error(...)", it will asumme it for the returned type (i.e.: Observable type). Later, when analysing - * the "else" clause, it will find another return type for the Observable.timer(...) method, which means a value of type Observable. At this time it does - * not know what to and will throw an error. So, this is why we need to cast the return line to the Observable type. - * - * See these links for details about lambda inference: - * - http://stackoverflow.com/questions/31227149/why-is-this-type-inference-not-working-with-this-lambda-expression-scenario - * - http://stackoverflow.com/questions/27508223/when-returning-a-list-of-custom-objects-rxjava-highlights-an-error-but-compiles - * - * ******** - * - * Note that we want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) only for subsequent errors, so, as soon as source emits a valid data, we reset - * our counter. Then, next time source emits an error, we will start counting errors from zero. - * - * @return - */ - private Subscription retryWhen_threeTimes() { - - return emitItemsAndThenError() - .compose(this.showDebugMessages("emitItemsAndThenError")) - .retryWhen(error -> error - .flatMap(throwable -> { - // When the number of retries hits MAX_ATTEMPTS (3 times) it will emit an error, otherwise it will delay the error for 1 second - // and emit it (this is actually what timer() operator does) - // - // According to the documentation: - // - when retryWhen() emits an error or completes, it stops resubscribing the source observable. - // - when retryWhen() emits any value (here value does not matter, but only the emitted type) it will resubscribe the - // source observable. - // - // So, this is how we control re-subscriptions and stops it according to our needs. - if (++attemptCount >= MAX_ATTEMPTS) { - Log.v(TAG, "retryWhen_threeTimes::retryWhen() - Reached max number of attempts (" + MAX_ATTEMPTS + "). Emitting an error..."); - return (Observable) Observable.error(new Throwable("Reached max number of attempts (" + MAX_ATTEMPTS + "). Emitting an error...")); - } else { - Log.v(TAG, "retryWhen_threeTimes::retryWhen() - Emitting an empty Observable. Attempt: " + attemptCount); - return (Observable) Observable.timer(1, TimeUnit.SECONDS); - } - }) - ) - - // Whenever we get here, it means source emitted an observable, so we need to reset attemptCount attribute. - // We just want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) for subsequent errors. - // So, as soon as source emits a valid data, we reset our counter. - .doOnNext(n -> { - Log.v(TAG, "retryWhen_threeTimes::doOnNext() - Reset attemptCount."); - attemptCount = 0; - }) - - .compose(this.showDebugMessages("retryWhen")) - .compose(applySchedulers()) - .subscribe( - value -> tvResult.setText(tvResult.getText() + " " + value), - err -> tvResult.setText(tvResult.getText() + " - doOnError: " + err), - () -> tvResult.setText(tvResult.getText() + " - onCompleted") - ); - } + @OnClick(R.id.btn_retry_when_with_exponential_backoff) + public void onRetryWhen_WithExponentialBackoffButtonClick() { - /** - * This example also flatmap's the error received from the retryWhen, but instead of use a class scope attribute to control the number of attempts, it uses - * zipWith() operator combined to range(). Then, it flatmap's the emitted items and sleep for a while prior the re-subscription. - * - * After range emits all its items, the chain is terminated. - * - * Note that we want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) only for subsequent errors, so, as soon as source emits a valid data, we reset - * our counter. Then, next time source emits an error, we will start counting errors from zero. - * - * @return - */ - private Subscription retryWhen_threeTimes_with_zipWith() { - - return emitItemsAndThenError() - .compose(this.showDebugMessages("emitItemsAndThenError")) - .retryWhen((error) -> { - Log.v(TAG, "retryWhen_threeTimes_with_zipWith::retryWhen()"); - - // The previous example (retryWhen_threeTimes) controls the number of retries by changing a class scope attribute (attemptCount) every time - // it resubscribes, but RxJava provides an operator that can do it: zipWith(). Basically it emits the source value (in this - // case the Throwable) combined with the first argument (in this case the values returned from the range() observable which will start from 1 - // and emit for two (MAX_ATTEMPTS) -1) times. This value is used in the next operator (flatMap) to sleep for 2 seconds prior to resubscribe - // source operator. - return error - .zipWith(Observable.range(COUNTER_START, MAX_ATTEMPTS -1), (throwable, attempt) -> { - Log.v(TAG, "retryWhen_threeTimes_with_zipWith::zipWith() - Attempt " + attempt); - return attempt; - }) - .flatMap(attempt -> Observable.timer(2, TimeUnit.SECONDS)); - }) - - // Whenever we get here, it means source emitted an observable, so we need to reset attemptCount attribute. - // We just want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) for subsequent errors. - // So, as soon as source emits a valid data, we reset our counter. - .doOnNext(n -> { - Log.v(TAG, "retryWhen_threeTimes_with_zipWith::doOnNext() - Reset attemptCount."); - attemptCount = 0; - }) - - .compose(this.showDebugMessages("retryWhen")) - .compose(applySchedulers()) - .subscribe( - value -> tvResult.setText(tvResult.getText() + " " + value), - error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), - () -> tvResult.setText(tvResult.getText() + " - onCompleted") - ); + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + resetData(); + subscription = retryWhen_withExponentialBackoff(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } - /** - * This example uses an exponential backoff to delay re-subscriptions as the error occurs. This is useful when we do not want to bother the server with - * retries, but gives it more time prior to retry. See retryMatrix construction (in onCreate() method) for timespan details. - * - * @return - */ - private Subscription retryWhen_withExponentialBackoff() { - - return emitItemsAndThenError() - .compose(this.showDebugMessages("emitItemsAndThenError")) - .retryWhen(error -> error - .flatMap( throwable -> Observable - .timer(getSecondsToSleep(++attemptCount), TimeUnit.SECONDS) - ) - ) - .compose(this.showDebugMessages("retryWhen")) - - // Whenever we get here, it means source emitted an observable, so we need to reset attemptCount attribute. - // We just want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) for subsequent errors. - // So, as soon as source emits a valid data, we reset our counter. - .doOnNext(n -> { - Log.v(TAG, "retryWhen_withExponentialBackoff::doOnNext() - Reset attemptCount."); - attemptCount = 0; - }) - - .compose(applySchedulers()) - .subscribe( - value -> tvResult.setText(tvResult.getText() + " " + value), - error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), - () -> tvResult.setText(tvResult.getText() + " - onCompleted") - ); + @OnClick(R.id.btn_stop_subscription) + public void onStopSubscription() { + if (subscription != null) { + subscription.unsubscribe(); } - - private Integer getSecondsToSleep(Integer attempt) { - Integer secondsToSleep = 0; - for( int i = 0; i < retryMatrix.size() && retryMatrix.get(i).first <= attempt; i++ ) { - secondsToSleep = retryMatrix.get(i).second; + } + + /** + * This method uses a random value in order to decide whether to emit either an observable or an error. + * + * Currently there is 20% of chance to emit and error and 80% of chance to emit an observable. + */ + private Observable emitItemsAndThenError() { + Log.v(TAG, "emitItemsAndThenError()"); + + return Observable.zip(Observable.range(0, 100), Observable.interval(100, TimeUnit.MILLISECONDS), + (a, b) -> a).flatMap(n -> { + Integer random = new Random().nextInt(10 - 1) + 1; + + if (random > 2) { + Log.v(TAG, "emitItemsAndThenError() - Emitting number: " + n); + updateEmittedItemView(n.toString()); + return Observable.just(n); + } else { + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Log.v(TAG, "emitItemsAndThenError() - Got an InterruptedException!"); } - - Log.v(TAG, "getSecondsToSleep() - attempt: " + attempt + " - secondsToSleep: " + secondsToSleep); - return secondsToSleep; + return Observable.error(new Throwable("Emitting an error")); + } + }); + } + + private Subscription retry_forever() { + + // Remember that emitItemsAndThenError() can emit an item or an error. When it emits an error, retry will act. + return emitItemsAndThenError().compose(this.showDebugMessages("emitItemsAndThenError")) + .retry() + .compose(this.showDebugMessages("retry")) + .compose(applySchedulers()) + .subscribe(value -> tvResult.setText(tvResult.getText() + " " + value), + error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), + () -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + private Subscription retryWhen_forever() { + + return emitItemsAndThenError().compose(this.showDebugMessages("emitItemsAndThenError")) + .retryWhen(error -> error + + // Right after retryWhen(...) subscription, it will stop here waiting for the source to emit an error. + .flatMap(error2 -> { + // Since we are always emitting source observable (the Throwable object) after timer() expires (in two seconds), this method will act + // exactly like the retry() operator. + Log.v(TAG, + "retryWhen_forever::flatMap() - Emitting throwable. This will make retryWhen to re-subscribe source observable. Attempt: " + + ++attemptCount); + return Observable.timer(2, TimeUnit.SECONDS) + .compose(RetryExampleActivity.this.showDebugMessages("retryWhen.timer")); + })) + .compose(this.showDebugMessages("retryWhen")) + .compose(applySchedulers()) + .subscribe(value -> tvResult.setText(tvResult.getText() + " " + value), + error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), + () -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + /** + * This example demonstrates how retryWhen SHOULD NOT be used. Since it does not react over the error emitted by the source, it terminates as soon as timer + * operator finishes its job (sleep for two seconds). + * + * See next three examples which uses flatMap in order to see how to use it in the right way. + */ + private Subscription retryWhen_no_flatMap() { + + return emitItemsAndThenError().compose(this.showDebugMessages("emitItemsAndThenError")) + .retryWhen((error) -> { + Log.v(TAG, "retryForever2::retryWhen()"); + + // Right after the subscription, it will get here, but since we are not reacting over the error emitted by + // the source, retryWhen() will terminate as soon as timer() operator returns (after 2 seconds) making source + // observable stops emitting and breaking the entire chain. + // + // This is actually not what we expect to see in a real application, but we added it here just to demonstrate how retryWhen actually behaves. + return error.timer(2, TimeUnit.SECONDS) + + // Just for log purpose + .compose(showDebugMessages("retryWhen.timer")); + }) + .compose(this.showDebugMessages("retryWhen")) + .compose(applySchedulers()) + .subscribe(value -> tvResult.setText(tvResult.getText() + " " + value), + error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), + () -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + /** + * Note on this example we must explicitly cast flatmap return type to the (Observable) type in the function applied to the emitted items. + * This is because inside that function we can return multiple types of Observables (an Observable until we reach MAX_ATTEMPTS or an Observable + * after we reach MAX_ATTEMPTS value). + * + * This is needed since the compiler resolves lambda expressions types by using the first statement that uses it. In our case, since it will first find + * the line: "return Observable.error(...)", it will asumme it for the returned type (i.e.: Observable type). Later, when analysing + * the "else" clause, it will find another return type for the Observable.timer(...) method, which means a value of type Observable. At this time it does + * not know what to and will throw an error. So, this is why we need to cast the return line to the Observable type. + * + * See these links for details about lambda inference: + * - http://stackoverflow.com/questions/31227149/why-is-this-type-inference-not-working-with-this-lambda-expression-scenario + * - http://stackoverflow.com/questions/27508223/when-returning-a-list-of-custom-objects-rxjava-highlights-an-error-but-compiles + * + * ******** + * + * Note that we want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) only for subsequent errors, so, as soon as source emits a valid data, we reset + * our counter. Then, next time source emits an error, we will start counting errors from zero. + */ + private Subscription retryWhen_threeTimes() { + + return emitItemsAndThenError().compose(this.showDebugMessages("emitItemsAndThenError")) + .retryWhen(error -> error.flatMap(throwable -> { + // When the number of retries hits MAX_ATTEMPTS (3 times) it will emit an error, otherwise it will delay the error for 1 second + // and emit it (this is actually what timer() operator does) + // + // According to the documentation: + // - when retryWhen() emits an error or completes, it stops resubscribing the source observable. + // - when retryWhen() emits any value (here value does not matter, but only the emitted type) it will resubscribe the + // source observable. + // + // So, this is how we control re-subscriptions and stops it according to our needs. + if (++attemptCount >= MAX_ATTEMPTS) { + Log.v(TAG, "retryWhen_threeTimes::retryWhen() - Reached max number of attempts (" + + MAX_ATTEMPTS + + "). Emitting an error..."); + return (Observable) Observable.error(new Throwable( + "Reached max number of attempts (" + MAX_ATTEMPTS + "). Emitting an error...")); + } else { + Log.v(TAG, "retryWhen_threeTimes::retryWhen() - Emitting an empty Observable. Attempt: " + + attemptCount); + return (Observable) Observable.timer(1, TimeUnit.SECONDS); + } + })) + + // Whenever we get here, it means source emitted an observable, so we need to reset attemptCount attribute. + // We just want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) for subsequent errors. + // So, as soon as source emits a valid data, we reset our counter. + .doOnNext(n -> { + Log.v(TAG, "retryWhen_threeTimes::doOnNext() - Reset attemptCount."); + attemptCount = 0; + }) + + .compose(this.showDebugMessages("retryWhen")) + .compose(applySchedulers()) + .subscribe(value -> tvResult.setText(tvResult.getText() + " " + value), + err -> tvResult.setText(tvResult.getText() + " - doOnError: " + err), + () -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + /** + * This example also flatmap's the error received from the retryWhen, but instead of use a class scope attribute to control the number of attempts, it uses + * zipWith() operator combined to range(). Then, it flatmap's the emitted items and sleep for a while prior the re-subscription. + * + * After range emits all its items, the chain is terminated. + * + * Note that we want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) only for subsequent errors, so, as soon as source emits a valid data, we reset + * our counter. Then, next time source emits an error, we will start counting errors from zero. + */ + private Subscription retryWhen_threeTimes_with_zipWith() { + + return emitItemsAndThenError().compose(this.showDebugMessages("emitItemsAndThenError")) + .retryWhen((error) -> { + Log.v(TAG, "retryWhen_threeTimes_with_zipWith::retryWhen()"); + + // The previous example (retryWhen_threeTimes) controls the number of retries by changing a class scope attribute (attemptCount) every time + // it resubscribes, but RxJava provides an operator that can do it: zipWith(). Basically it emits the source value (in this + // case the Throwable) combined with the first argument (in this case the values returned from the range() observable which will start from 1 + // and emit for two (MAX_ATTEMPTS) -1) times. This value is used in the next operator (flatMap) to sleep for 2 seconds prior to resubscribe + // source operator. + return error.zipWith(Observable.range(COUNTER_START, MAX_ATTEMPTS - 1), + (throwable, attempt) -> { + Log.v(TAG, "retryWhen_threeTimes_with_zipWith::zipWith() - Attempt " + attempt); + return attempt; + }).flatMap(attempt -> Observable.timer(2, TimeUnit.SECONDS)); + }) + + // Whenever we get here, it means source emitted an observable, so we need to reset attemptCount attribute. + // We just want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) for subsequent errors. + // So, as soon as source emits a valid data, we reset our counter. + .doOnNext(n -> { + Log.v(TAG, "retryWhen_threeTimes_with_zipWith::doOnNext() - Reset attemptCount."); + attemptCount = 0; + }) + + .compose(this.showDebugMessages("retryWhen")) + .compose(applySchedulers()) + .subscribe(value -> tvResult.setText(tvResult.getText() + " " + value), + error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), + () -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + /** + * This example uses an exponential backoff to delay re-subscriptions as the error occurs. This is useful when we do not want to bother the server with + * retries, but gives it more time prior to retry. See retryMatrix construction (in onCreate() method) for timespan details. + */ + private Subscription retryWhen_withExponentialBackoff() { + + return emitItemsAndThenError().compose(this.showDebugMessages("emitItemsAndThenError")) + .retryWhen(error -> error.flatMap( + throwable -> Observable.timer(getSecondsToSleep(++attemptCount), TimeUnit.SECONDS))) + .compose(this.showDebugMessages("retryWhen")) + + // Whenever we get here, it means source emitted an observable, so we need to reset attemptCount attribute. + // We just want to retry at most for MAX_ATTEMPTS times (i.e. 3 times) for subsequent errors. + // So, as soon as source emits a valid data, we reset our counter. + .doOnNext(n -> { + Log.v(TAG, "retryWhen_withExponentialBackoff::doOnNext() - Reset attemptCount."); + attemptCount = 0; + }) + + .compose(applySchedulers()) + .subscribe(value -> tvResult.setText(tvResult.getText() + " " + value), + error -> tvResult.setText(tvResult.getText() + " - doOnError: " + error), + () -> tvResult.setText(tvResult.getText() + " - onCompleted")); + } + + private Integer getSecondsToSleep(Integer attempt) { + Integer secondsToSleep = 0; + for (int i = 0; i < retryMatrix.size() && retryMatrix.get(i).first <= attempt; i++) { + secondsToSleep = retryMatrix.get(i).second; } - /** - * Created this helper method only to allow our lambda expressions to become even shorter. - * - * @param// text - */ - private void updateEmittedItemView(String text) { - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + text)); - } + Log.v(TAG, + "getSecondsToSleep() - attempt: " + attempt + " - secondsToSleep: " + secondsToSleep); + return secondsToSleep; + } + + /** + * Created this helper method only to allow our lambda expressions to become even shorter. + * + * @param// text + */ + private void updateEmittedItemView(String text) { + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + text)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TimeoutExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TimeoutExampleActivity.java index 18a4324..278d6fc 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TimeoutExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TimeoutExampleActivity.java @@ -5,16 +5,13 @@ import android.util.Log; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -22,371 +19,350 @@ public class TimeoutExampleActivity extends BaseActivity { - private static final String TAG = TimeoutExampleActivity.class.getSimpleName(); + private static final String TAG = TimeoutExampleActivity.class.getSimpleName(); - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_timeout_example); - ButterKnife.bind(this); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_timeout_example); + ButterKnife.bind(this); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private void resetData() { - tvEmittedNumbers.setText(""); - tvResult.setText(""); + } + + private void resetData() { + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_start_timeout_operator_test) + public void onStartTimeoutTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTimeoutTestButtonClick()"); + resetData(); + subscription = startTimeoutTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_timeout_operator_test) - public void onStartTimeoutTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTimeoutTestButtonClick()"); - resetData(); - subscription = startTimeoutTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_timeout_operator_with_second_observable_test) + public void onStartTimeoutWithSecondObservableTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTimeoutWithSecondObservableTestButtonClick()"); + resetData(); + subscription = startTimeoutWithSecondObservableTest(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_timeout_operator_with_second_observable_test) - public void onStartTimeoutWithSecondObservableTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTimeoutWithSecondObservableTestButtonClick()"); - resetData(); - subscription = startTimeoutWithSecondObservableTest(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_timeout_operator_with_a_function_test) + public void onStartTimeoutWithFunctionTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTimeoutWithFunctionTestButtonClick()"); + resetData(); + subscription = startTimeoutWithFunction(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_timeout_operator_with_a_function_test) - public void onStartTimeoutWithFunctionTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTimeoutWithFunctionTestButtonClick()"); - resetData(); - subscription = startTimeoutWithFunction(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_timeout_operator_with_a_function_and_a_second_observable_test) + public void onStartTimeoutWithFunctionAndSecondObservableTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTimeoutWithFunctionAndSecondObservableTestButtonClick()"); + resetData(); + subscription = startTimeoutWithFunctionAndSecondObservable(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_timeout_operator_with_a_function_and_a_second_observable_test) - public void onStartTimeoutWithFunctionAndSecondObservableTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTimeoutWithFunctionAndSecondObservableTestButtonClick()"); - resetData(); - subscription = startTimeoutWithFunctionAndSecondObservable(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_timeout_operator_with_a_function_and_a_second_function_test) + public void onStartTimeoutWithFunctionAndSecondFunctionTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTimeoutWithFunctionAndSecondFunctionTestButtonClick()"); + resetData(); + subscription = + startTimeoutWithAFunctionForTheFirstEmittedItemAndAnotherFunctionForTheOtherItems(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_timeout_operator_with_a_function_and_a_second_function_test) - public void onStartTimeoutWithFunctionAndSecondFunctionTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTimeoutWithFunctionAndSecondFunctionTestButtonClick()"); - resetData(); - subscription = startTimeoutWithAFunctionForTheFirstEmittedItemAndAnotherFunctionForTheOtherItems(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_timeout_operator_with_a_function_a_second_function_and_a_second_observable_test) + public void onStartTimeoutWithFunctionAndSecondFunctionAndSecondObservableTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartTimeoutWithFunctionAndSecondFunctionAndSecondObservableTestButtonClick()"); + resetData(); + subscription = startTimeoutWithFunctionAndASecondFunctionAndASecondObservable(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - @OnClick(R.id.btn_start_timeout_operator_with_a_function_a_second_function_and_a_second_observable_test) - public void onStartTimeoutWithFunctionAndSecondFunctionAndSecondObservableTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartTimeoutWithFunctionAndSecondFunctionAndSecondObservableTestButtonClick()"); - resetData(); - subscription = startTimeoutWithFunctionAndASecondFunctionAndASecondObservable(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + } + + @OnClick(R.id.btn_start_bonus_test) + public void onStartBonusTestButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onStartBonusTestButtonClick()"); + resetData(); + subscription = startBonusTest_TimeoutWithRetry(); + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } + private Observable emitItems(Integer numberOfItems) { + Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); - @OnClick(R.id.btn_start_bonus_test) - public void onStartBonusTestButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onStartBonusTestButtonClick()"); - resetData(); - subscription = startBonusTest_TimeoutWithRetry(); - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } - - private Observable emitItems(Integer numberOfItems) { - Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + return Observable - return Observable + // Emit N items based on the "numberOfItems" parameter + .range(0, numberOfItems) - // Emit N items based on the "numberOfItems" parameter - .range(0, numberOfItems) + .doOnNext((number) -> { + try { + int timeout = new Random().nextInt(700 - 100) + 100; - .doOnNext((number) -> { - try { - int timeout = new Random().nextInt(700 - 100) + 100; + Log.v(TAG, + "Item: " + number + " will be emitted with a delay around: " + timeout + "ms"); + Thread.sleep(timeout); + Log.v(TAG, "emitItems() - Emitting number: " + number); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - Log.v(TAG, "Item: " + number + " will be emitted with a delay around: " + timeout + "ms"); - Thread.sleep(timeout); - Log.v(TAG, "emitItems() - Emitting number: " + number); + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }); + } - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } + /** + * Useful for the tests that uses a second observable in case of a timeout. + */ + private Observable emitSecondItems() { + Log.v(TAG, "emitSecondItems()"); - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }); - } + return Observable.range(100, 5).doOnNext((number) -> { + try { + Log.v(TAG, "emitSecondItems() - Emitting number: " + number); + Thread.sleep(100); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } - /** - * Useful for the tests that uses a second observable in case of a timeout. - * - * @return - */ - private Observable emitSecondItems() { - Log.v(TAG, "emitSecondItems()"); - - return Observable - .range(100, 5) - .doOnNext((number) -> { - try { - Log.v(TAG, "emitSecondItems() - Emitting number: " + number); - Thread.sleep(100); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }); - } + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }); + } - /** - * If source takes more than 500ms to emit, an error is emitted. - * - * @return - */ - private Subscription startTimeoutTest() { + /** + * If source takes more than 500ms to emit, an error is emitted. + */ + private Subscription startTimeoutTest() { - return Observable - .timer(0, TimeUnit.SECONDS) + return Observable.timer(0, TimeUnit.SECONDS) - .flatMap((i) -> emitItems(20)) + .flatMap((i) -> emitItems(20)) - .timeout(500, TimeUnit.MILLISECONDS) + .timeout(500, TimeUnit.MILLISECONDS) - // Just for log purpose - .compose(showDebugMessages("timeout")) + // Just for log purpose + .compose(showDebugMessages("timeout")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * This example will trigger a second observable in case of the first observable takes more than 500ms to emit - * - * @return - */ - private Subscription startTimeoutWithSecondObservableTest() { + /** + * This example will trigger a second observable in case of the first observable takes more than 500ms to emit + */ + private Subscription startTimeoutWithSecondObservableTest() { - return Observable - .timer(0, TimeUnit.SECONDS) + return Observable.timer(0, TimeUnit.SECONDS) - .flatMap((i) -> emitItems(20)) + .flatMap((i) -> emitItems(20)) - .timeout(500, TimeUnit.MILLISECONDS, emitSecondItems()) + .timeout(500, TimeUnit.MILLISECONDS, emitSecondItems()) - // Just for log purpose - .compose(showDebugMessages("timeout(300ms)")) + // Just for log purpose + .compose(showDebugMessages("timeout(300ms)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * This example applies different timeouts based on whether emitted items are even or odd - * - * @return - */ - private Subscription startTimeoutWithFunction() { + /** + * This example applies different timeouts based on whether emitted items are even or odd + */ + private Subscription startTimeoutWithFunction() { - return Observable - .timer(0, TimeUnit.SECONDS) + return Observable.timer(0, TimeUnit.SECONDS) - .flatMap((i) -> emitItems(20)) + .flatMap((i) -> emitItems(20)) - .timeout((number) -> { - // For even emitted numbers, a 300ms timeout is applied, otherwise 600ms. - if (number % 2 == 0) { - return Observable.timer(300, TimeUnit.MILLISECONDS); - } else { - return Observable.timer(600, TimeUnit.MILLISECONDS); - } - }) + .timeout((number) -> { + // For even emitted numbers, a 300ms timeout is applied, otherwise 600ms. + if (number % 2 == 0) { + return Observable.timer(300, TimeUnit.MILLISECONDS); + } else { + return Observable.timer(600, TimeUnit.MILLISECONDS); + } + }) - // Just for log purpose - .compose(showDebugMessages("timeout(w/function)")) + // Just for log purpose + .compose(showDebugMessages("timeout(w/function)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * This example is basically a combination of the two previous examples: startTimeoutWithSecondObservableTest and startTimeoutWithFunction - * - * @return - */ - private Subscription startTimeoutWithFunctionAndSecondObservable() { + /** + * This example is basically a combination of the two previous examples: startTimeoutWithSecondObservableTest and startTimeoutWithFunction + */ + private Subscription startTimeoutWithFunctionAndSecondObservable() { - return Observable - .timer(0, TimeUnit.SECONDS) + return Observable.timer(0, TimeUnit.SECONDS) - .flatMap((i) -> emitItems(20)) + .flatMap((i) -> emitItems(20)) - .timeout((number) -> { - // For even emitted numbers, a timeout of 300ms is used, otherwise 600ms. For any item, in case of timeout, a second observable is triggered. - if (number % 2 == 0) { - return Observable.timer(300, TimeUnit.MILLISECONDS); - } else { - return Observable.timer(600, TimeUnit.MILLISECONDS); - } - }, emitSecondItems()) + .timeout((number) -> { + // For even emitted numbers, a timeout of 300ms is used, otherwise 600ms. For any item, in case of timeout, a second observable is triggered. + if (number % 2 == 0) { + return Observable.timer(300, TimeUnit.MILLISECONDS); + } else { + return Observable.timer(600, TimeUnit.MILLISECONDS); + } + }, emitSecondItems()) - // Just for log purpose - .compose(showDebugMessages("timeout(w/function and a second observable)")) + // Just for log purpose + .compose(showDebugMessages("timeout(w/function and a second observable)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * This example applies a function for the first item by defining a timeout only for it. Then, it defines a second function - * that will be applied to all remaining items. - * - * @return - */ - private Subscription startTimeoutWithAFunctionForTheFirstEmittedItemAndAnotherFunctionForTheOtherItems() { + /** + * This example applies a function for the first item by defining a timeout only for it. Then, it defines a second function + * that will be applied to all remaining items. + */ + private Subscription startTimeoutWithAFunctionForTheFirstEmittedItemAndAnotherFunctionForTheOtherItems() { - return Observable - .timer(0, TimeUnit.SECONDS) + return Observable.timer(0, TimeUnit.SECONDS) - .flatMap((i) -> emitItems(20)) + .flatMap((i) -> emitItems(20)) - .timeout(() -> + .timeout(() -> - // This function will be applied only for the first emitted item - Observable.timer(6500, TimeUnit.MILLISECONDS), - (number) -> { - // For all the other emitted items, for even numbers, a timeout of 300ms is used, otherwise 600ms. - if (number % 2 == 0) { - return Observable.timer(300, TimeUnit.MILLISECONDS); - } else { - return Observable.timer(600, TimeUnit.MILLISECONDS); - } - }) + // This function will be applied only for the first emitted item + Observable.timer(6500, TimeUnit.MILLISECONDS), (number) -> { + // For all the other emitted items, for even numbers, a timeout of 300ms is used, otherwise 600ms. + if (number % 2 == 0) { + return Observable.timer(300, TimeUnit.MILLISECONDS); + } else { + return Observable.timer(600, TimeUnit.MILLISECONDS); + } + }) - // Just for log purpose - .compose(showDebugMessages("timeout(w/function and a second function)")) + // Just for log purpose + .compose(showDebugMessages("timeout(w/function and a second function)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - private Subscription startTimeoutWithFunctionAndASecondFunctionAndASecondObservable() { + private Subscription startTimeoutWithFunctionAndASecondFunctionAndASecondObservable() { - return Observable - .timer(0, TimeUnit.SECONDS) + return Observable.timer(0, TimeUnit.SECONDS) - .flatMap((i) -> emitItems(20)) + .flatMap((i) -> emitItems(20)) - .timeout(() -> + .timeout(() -> // This function will be applied only for the first emitted item. - Observable.timer(500, TimeUnit.MILLISECONDS), - (number) -> { - // For all the other emitted items, for even numbers, a timeout of 300ms is used, otherwise 600ms. - if (number % 2 == 0) { - return Observable.timer(300, TimeUnit.MILLISECONDS); - } else { - return Observable.timer(600, TimeUnit.MILLISECONDS); - } - }, - - // In case of timeout on any item (i.e. the first one that uses an exclusive function or any other), this - // observable will be used instead of terminating with an error. - emitSecondItems()) - - // Just for log purpose - .compose(showDebugMessages("timeout(w/function, a second function and a second observable)")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } - - /** - * This is a bonus example. If source takes longer than 500ms to emit items, a timeout will be thrown and retryWhen will delay 5 seconds prior to emit. It source throws error for - * three times, retryWhen will emit an error and the whole chain will finish - * - */ - private int attemptCount= 0; - private static final int MAX_ATTEMPTS = 3; - private Subscription startBonusTest_TimeoutWithRetry() { - - attemptCount= 0; - - return Observable - .timer(0, TimeUnit.SECONDS) - - .flatMap((i) -> emitItems(20)) - - .timeout(500, TimeUnit.MILLISECONDS) - - .retryWhen(error -> error - .flatMap(throwable -> { - if (++attemptCount >= MAX_ATTEMPTS) { - return (Observable) Observable.error(new Throwable("Reached max number of attempts (" + MAX_ATTEMPTS + "). Emitting an error...")); - } else { - Log.v(TAG, "Wait for 5 seconds prior to retry"); - return (Observable) Observable.timer(5, TimeUnit.SECONDS); - } - }) - ) - - // Just for log purpose - .compose(showDebugMessages("timeout(w/function, a second function and a second observable)")) - - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) - - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + Observable.timer(500, TimeUnit.MILLISECONDS), (number) -> { + // For all the other emitted items, for even numbers, a timeout of 300ms is used, otherwise 600ms. + if (number % 2 == 0) { + return Observable.timer(300, TimeUnit.MILLISECONDS); + } else { + return Observable.timer(600, TimeUnit.MILLISECONDS); + } + }, + + // In case of timeout on any item (i.e. the first one that uses an exclusive function or any other), this + // observable will be used instead of terminating with an error. + emitSecondItems()) + + // Just for log purpose + .compose( + showDebugMessages("timeout(w/function, a second function and a second observable)")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } + + /** + * This is a bonus example. If source takes longer than 500ms to emit items, a timeout will be thrown and retryWhen will delay 5 seconds prior to emit. It source throws error for + * three times, retryWhen will emit an error and the whole chain will finish + */ + private int attemptCount = 0; + private static final int MAX_ATTEMPTS = 3; + + private Subscription startBonusTest_TimeoutWithRetry() { + + attemptCount = 0; + + return Observable.timer(0, TimeUnit.SECONDS) + + .flatMap((i) -> emitItems(20)) + + .timeout(500, TimeUnit.MILLISECONDS) + + .retryWhen(error -> error.flatMap(throwable -> { + if (++attemptCount >= MAX_ATTEMPTS) { + return (Observable) Observable.error(new Throwable( + "Reached max number of attempts (" + MAX_ATTEMPTS + "). Emitting an error...")); + } else { + Log.v(TAG, "Wait for 5 seconds prior to retry"); + return (Observable) Observable.timer(5, TimeUnit.SECONDS); + } + })) + + // Just for log purpose + .compose( + showDebugMessages("timeout(w/function, a second function and a second observable)")) + + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) + + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TransformingOperatorsExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TransformingOperatorsExampleActivity.java index 2197bcf..18307bc 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TransformingOperatorsExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/operators/TransformingOperatorsExampleActivity.java @@ -8,21 +8,18 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.OnItemSelected; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import butterknife.OnItemSelected; import rx.Observable; import rx.Scheduler; import rx.Subscription; @@ -30,437 +27,435 @@ public class TransformingOperatorsExampleActivity extends BaseActivity { - class SpinnerOptionAndMethodName extends Pair { - - public SpinnerOptionAndMethodName(String first, String second) { - super(first, second); - } + class SpinnerOptionAndMethodName extends Pair { - @Override - public String toString() { - return (String) first; - } + public SpinnerOptionAndMethodName(String first, String second) { + super(first, second); } - private static final String TAG = TransformingOperatorsExampleActivity.class.getSimpleName(); - - @BindView(R.id.tv_emitted_numbers) TextView tvEmittedNumbers; - @BindView(R.id.tv_result) TextView tvResult; - @BindView(R.id.s_test_options) Spinner sTestOptions; - - // Hold the method name related to the test user chosen in the spinner control. - private String currentTestMethodName; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_operators_transforming_operators_example); - ButterKnife.bind(this); - - // Fill spinner view up with all available test names and their related method names. We will use reflection to call a method based on the user choice - List testOptions = new ArrayList<>(); - testOptions.add(new SpinnerOptionAndMethodName("buffer() w/count", "startBufferOperatorTestWithCount")); - testOptions.add(new SpinnerOptionAndMethodName("buffer() w/timespan", "startBufferOperatorTestWithTimespan")); - testOptions.add(new SpinnerOptionAndMethodName("buffer() w/count and timespan", "startBufferOperatorTestWithCountAndTimespan")); - testOptions.add(new SpinnerOptionAndMethodName("buffer() w/timespan and timeshift", "startBufferOperatorTestWithTimespanAndTimeshift")); - testOptions.add(new SpinnerOptionAndMethodName("buffer() w/boundary", "startBufferOperatorTestWithBoundary")); - testOptions.add(new SpinnerOptionAndMethodName("buffer() w/selector", "startBufferOperatorTestWithSelector")); - testOptions.add(new SpinnerOptionAndMethodName("window() w/count and timespan", "startWindowOperatorTestWithCountAndTimespan")); - testOptions.add(new SpinnerOptionAndMethodName("scan()", "startScanOperatorTest")); - testOptions.add(new SpinnerOptionAndMethodName("scan() w/seed", "startScanOperatorWithSeedTest")); - - // Do not show this example, since it is entirely based on a great GIST. See comments below - //testOptions.add(new SpinnerOptionAndMethodName("scan() Fibonacci", "startScanOperatorFibonacciTest")); - - - ArrayAdapter adapter = new ArrayAdapter<>( - this, android.R.layout.simple_spinner_item, testOptions); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - sTestOptions.setAdapter(adapter); - - // Set the default method name - currentTestMethodName = "startFirstOperatorTest"; - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - } - - @OnItemSelected(R.id.s_test_options) - public void spinnerTestOptionsItemSelected(Spinner spinner, int position) { - SpinnerOptionAndMethodName testItem = (SpinnerOptionAndMethodName) spinner.getAdapter().getItem(position); - - currentTestMethodName = (String) testItem.second; - - tvEmittedNumbers.setText(""); - tvResult.setText(""); - } - - @OnClick(R.id.btn_start_test) - public void onButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onButtonClick()"); - - tvEmittedNumbers.setText(""); - tvResult.setText(""); - - try { - - // Instantiate an object of type method that returns a method name we will invoke - Method m = this.getClass().getDeclaredMethod(currentTestMethodName); - - // Now, invoke method user selected - subscription = (Subscription) m.invoke(this); - - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } + public String toString() { + return (String) first; } - - private Observable emitItems(Integer numberOfItems, boolean randomItems, boolean randomDelay) { - Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); - - return Observable - - // Emit N items based on the "numberOfItems" parameter - .range(0, numberOfItems) - - // Generate a random number (for each emitted item) - .map((num) -> { - if (randomItems) { - return new Random().nextInt(10); - } else { - return num; - } - }) - - .doOnNext((number) -> { - try { - if (randomDelay) { - // Sleep for sometime between 100 and 300 milliseconds - Thread.sleep(new Random().nextInt(300 - 100) + 100); - } else { - // Sleep for 200ms - Thread.sleep(200); - } - Log.v(TAG, "emitItems() - Emitting number: " + number); - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); - }); + } + + private static final String TAG = TransformingOperatorsExampleActivity.class.getSimpleName(); + + @BindView(R.id.tv_emitted_numbers) + TextView tvEmittedNumbers; + @BindView(R.id.tv_result) + TextView tvResult; + @BindView(R.id.s_test_options) + Spinner sTestOptions; + + // Hold the method name related to the test user chosen in the spinner control. + private String currentTestMethodName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_operators_transforming_operators_example); + ButterKnife.bind(this); + + // Fill spinner view up with all available test names and their related method names. We will use reflection to call a method based on the user choice + List testOptions = new ArrayList<>(); + testOptions.add( + new SpinnerOptionAndMethodName("buffer() w/count", "startBufferOperatorTestWithCount")); + testOptions.add(new SpinnerOptionAndMethodName("buffer() w/timespan", + "startBufferOperatorTestWithTimespan")); + testOptions.add(new SpinnerOptionAndMethodName("buffer() w/count and timespan", + "startBufferOperatorTestWithCountAndTimespan")); + testOptions.add(new SpinnerOptionAndMethodName("buffer() w/timespan and timeshift", + "startBufferOperatorTestWithTimespanAndTimeshift")); + testOptions.add(new SpinnerOptionAndMethodName("buffer() w/boundary", + "startBufferOperatorTestWithBoundary")); + testOptions.add(new SpinnerOptionAndMethodName("buffer() w/selector", + "startBufferOperatorTestWithSelector")); + testOptions.add(new SpinnerOptionAndMethodName("window() w/count and timespan", + "startWindowOperatorTestWithCountAndTimespan")); + testOptions.add(new SpinnerOptionAndMethodName("scan()", "startScanOperatorTest")); + testOptions.add( + new SpinnerOptionAndMethodName("scan() w/seed", "startScanOperatorWithSeedTest")); + + // Do not show this example, since it is entirely based on a great GIST. See comments below + //testOptions.add(new SpinnerOptionAndMethodName("scan() Fibonacci", "startScanOperatorFibonacciTest")); + + ArrayAdapter adapter = + new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, testOptions); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + sTestOptions.setAdapter(adapter); + + // Set the default method name + currentTestMethodName = "startFirstOperatorTest"; + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - - private Observable doAContinuouslyOperation(boolean showDoneMessage) { - Log.v(TAG, "doAContinuouslyOperation()"); - - return Observable.interval(100, TimeUnit.MILLISECONDS) - .map((number) -> number.intValue()) - .doOnNext((number) -> { - try { - runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Starting operation", Toast.LENGTH_SHORT).show()); - - Integer timeToSleep = new Random().nextInt(6 - 3) + 3; - Log.v(TAG, "doAContinuouslyOperation() - Sleeping for " + timeToSleep + " second(s)"); - Thread.sleep(timeToSleep * 1000); - - if (showDoneMessage) { - runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Operation done", Toast.LENGTH_SHORT).show()); - } - - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }).take(1); + } + + @OnItemSelected(R.id.s_test_options) + public void spinnerTestOptionsItemSelected(Spinner spinner, int position) { + SpinnerOptionAndMethodName testItem = + (SpinnerOptionAndMethodName) spinner.getAdapter().getItem(position); + + currentTestMethodName = (String) testItem.second; + + tvEmittedNumbers.setText(""); + tvResult.setText(""); + } + + @OnClick(R.id.btn_start_test) + public void onButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onButtonClick()"); + + tvEmittedNumbers.setText(""); + tvResult.setText(""); + + try { + + // Instantiate an object of type method that returns a method name we will invoke + Method m = this.getClass().getDeclaredMethod(currentTestMethodName); + + // Now, invoke method user selected + subscription = (Subscription) m.invoke(this); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } + } + + private Observable emitItems(Integer numberOfItems, boolean randomItems, + boolean randomDelay) { + Log.v(TAG, "emitItems() - numberOfItems: " + numberOfItems); + + return Observable + + // Emit N items based on the "numberOfItems" parameter + .range(0, numberOfItems) + + // Generate a random number (for each emitted item) + .map((num) -> { + if (randomItems) { + return new Random().nextInt(10); + } else { + return num; + } + }) + + .doOnNext((number) -> { + try { + if (randomDelay) { + // Sleep for sometime between 100 and 300 milliseconds + Thread.sleep(new Random().nextInt(300 - 100) + 100); + } else { + // Sleep for 200ms + Thread.sleep(200); + } + Log.v(TAG, "emitItems() - Emitting number: " + number); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> tvEmittedNumbers.setText(tvEmittedNumbers.getText() + " " + number)); + }); + } + + private Observable doAContinuouslyOperation(boolean showDoneMessage) { + Log.v(TAG, "doAContinuouslyOperation()"); + + return Observable.interval(100, TimeUnit.MILLISECONDS) + .map((number) -> number.intValue()) + .doOnNext((number) -> { + try { + runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Starting operation", + Toast.LENGTH_SHORT).show()); + + Integer timeToSleep = new Random().nextInt(6 - 3) + 3; + Log.v(TAG, "doAContinuouslyOperation() - Sleeping for " + timeToSleep + " second(s)"); + Thread.sleep(timeToSleep * 1000); + + if (showDoneMessage) { + runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Operation done", + Toast.LENGTH_SHORT).show()); + } + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }) + .take(1); + } - private Observable doASecondContinuouslyOperation() { - Log.v(TAG, "doASecondContinuouslyOperation()"); - - return Observable.just(1) - .doOnNext((number) -> { - try { - runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Starting a second operation. Items will be buffered", Toast.LENGTH_SHORT).show()); - - Integer timeToSleep = new Random().nextInt(6 - 3) + 3; - Log.v(TAG, "doASecondContinuouslyOperation() - Sleeping for " + timeToSleep + " second(s)"); - Thread.sleep(timeToSleep * 1000); - - runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Second operation done", Toast.LENGTH_SHORT).show()); + private Observable doASecondContinuouslyOperation() { + Log.v(TAG, "doASecondContinuouslyOperation()"); - } catch (InterruptedException e) { - Log.v(TAG, "Got an InterruptedException!"); - } - }); - } + return Observable.just(1).doOnNext((number) -> { + try { + runOnUiThread(() -> Toast.makeText(getApplicationContext(), + "Starting a second operation. Items will be buffered", Toast.LENGTH_SHORT).show()); + Integer timeToSleep = new Random().nextInt(6 - 3) + 3; + Log.v(TAG, "doASecondContinuouslyOperation() - Sleeping for " + timeToSleep + " second(s)"); + Thread.sleep(timeToSleep * 1000); + runOnUiThread(() -> Toast.makeText(getApplicationContext(), "Second operation done", + Toast.LENGTH_SHORT).show()); + } catch (InterruptedException e) { + Log.v(TAG, "Got an InterruptedException!"); + } + }); + } - /** - * This example will emit groups of 4 items, no matter of how long delay between emitted items is. - * - * @return - */ - private Subscription startBufferOperatorTestWithCount() { + /** + * This example will emit groups of 4 items, no matter of how long delay between emitted items is. + */ + private Subscription startBufferOperatorTestWithCount() { - return emitItems(18, false, true) + return emitItems(18, false, true) - .buffer(4) + .buffer(4) - // Just for log purpose - .compose(showDebugMessages("buffer(4)")) + // Just for log purpose + .compose(showDebugMessages("buffer(4)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * This example will emit group of items that is emitted in a window of 700ms. Since emitted items will be delayed randomically, there is - * no way to know how many items will be grouped together. - * - * @return - */ - private Subscription startBufferOperatorTestWithTimespan() { + /** + * This example will emit group of items that is emitted in a window of 700ms. Since emitted items will be delayed randomically, there is + * no way to know how many items will be grouped together. + */ + private Subscription startBufferOperatorTestWithTimespan() { - return emitItems(20, false, true) + return emitItems(20, false, true) - .buffer(700, TimeUnit.MILLISECONDS) + .buffer(700, TimeUnit.MILLISECONDS) - // Just for log purpose - .compose(showDebugMessages("buffer(700ms)")) + // Just for log purpose + .compose(showDebugMessages("buffer(700ms)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * - * This example will emit group of four items or N items that is emitted in a window of 700ms, which comes first. - * - * - * @return - */ - private Subscription startBufferOperatorTestWithCountAndTimespan() { + /** + * This example will emit group of four items or N items that is emitted in a window of 700ms, which comes first. + */ + private Subscription startBufferOperatorTestWithCountAndTimespan() { - // Source will emit sequential numbers, and using a fixed delay of 200ms between each emission - return emitItems(20, false, false) + // Source will emit sequential numbers, and using a fixed delay of 200ms between each emission + return emitItems(20, false, false) - .buffer(700, TimeUnit.MILLISECONDS, 4) + .buffer(700, TimeUnit.MILLISECONDS, 4) - // Just for log purpose - .compose(showDebugMessages("buffer(700ms or 4 items [which comes first])")) + // Just for log purpose + .compose(showDebugMessages("buffer(700ms or 4 items [which comes first])")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - private Subscription startBufferOperatorTestWithTimespanAndTimeshift() { + private Subscription startBufferOperatorTestWithTimespanAndTimeshift() { - return emitItems(40, false, false) + return emitItems(40, false, false) - .buffer(600, 3000, TimeUnit.MILLISECONDS) + .buffer(600, 3000, TimeUnit.MILLISECONDS) - // Just for log purpose - .compose(showDebugMessages("buffer(600ms, 3000ms)")) + // Just for log purpose + .compose(showDebugMessages("buffer(600ms, 3000ms)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - private Subscription startBufferOperatorTestWithBoundary() { + private Subscription startBufferOperatorTestWithBoundary() { - return emitItems(40, false, true) + return emitItems(40, false, true) - .buffer(doAContinuouslyOperation(true)) + .buffer(doAContinuouslyOperation(true)) - // Just for log purpose - .compose(showDebugMessages("buffer(w/boundary)")) + // Just for log purpose + .compose(showDebugMessages("buffer(w/boundary)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - private Subscription startBufferOperatorTestWithSelector() { + private Subscription startBufferOperatorTestWithSelector() { - return emitItems(70, false, true) + return emitItems(70, false, true) - .buffer(doAContinuouslyOperation(false), (taskDone) -> doASecondContinuouslyOperation()) + .buffer(doAContinuouslyOperation(false), (taskDone) -> doASecondContinuouslyOperation()) - // Just for log purpose - .compose(showDebugMessages("buffer(w/selectors)")) + // Just for log purpose + .compose(showDebugMessages("buffer(w/selectors)")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - private Subscription startWindowOperatorTestWithCountAndTimespan() { + private Subscription startWindowOperatorTestWithCountAndTimespan() { - return emitItems(30, false, false) + return emitItems(30, false, false) - .window(700, TimeUnit.MILLISECONDS, 4) + .window(700, TimeUnit.MILLISECONDS, 4) - .compose(showDebugMessages("window()")) + .compose(showDebugMessages("window()")) - .flatMap(o -> o.toList()) + .flatMap(o -> o.toList()) - // Just for log purpose - .compose(showDebugMessages("window(700ms or 4 items [which comes first])")) + // Just for log purpose + .compose(showDebugMessages("window(700ms or 4 items [which comes first])")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * - * @return - */ - private Subscription startScanOperatorTest() { + /** + * + * @return + */ + private Subscription startScanOperatorTest() { - return emitItems(20, true, true) + return emitItems(20, true, true) - .scan((accumulator, item) -> { - if (item % 2 == 0) { - return accumulator + item; - } + .scan((accumulator, item) -> { + if (item % 2 == 0) { + return accumulator + item; + } - return accumulator; - }) + return accumulator; + }) - // Just for log purpose - .compose(showDebugMessages("scan")) + // Just for log purpose + .compose(showDebugMessages("scan")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * This example uses a variant of scan() operator which accepts a seed value that is applied to the first emitted item. - * - * When using a seed with a value of 3, it will be applied to the accumulator (only for the fist item) - * - * @return - */ - private Subscription startScanOperatorWithSeedTest() { + /** + * This example uses a variant of scan() operator which accepts a seed value that is applied to the first emitted item. + * + * When using a seed with a value of 3, it will be applied to the accumulator (only for the fist item) + */ + private Subscription startScanOperatorWithSeedTest() { - Integer seed = 3; + Integer seed = 3; - return emitItems(10, false, true) + return emitItems(10, false, true) - .scan(seed, (accumulator, item) -> { - Log.v(TAG, "scan() - seed: " + seed + " - accumulator: " + accumulator + " - item: " + item); - return accumulator + item; - }) + .scan(seed, (accumulator, item) -> { + Log.v(TAG, + "scan() - seed: " + seed + " - accumulator: " + accumulator + " - item: " + item); + return accumulator + item; + }) - // Just for log purpose - .compose(showDebugMessages("scan() w/seed")) + // Just for log purpose + .compose(showDebugMessages("scan() w/seed")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } - /** - * Fibonacci sequence using scan() operator. This example was based on the link below: - * - * https://gist.github.com/benjchristensen/a962c4bfa8adae286daf - * - * It is here just for demonstration purpose. It is not even being called anywhere on this app - * - * @return - */ - private Subscription startScanOperatorFibonacciTest() { + /** + * Fibonacci sequence using scan() operator. This example was based on the link below: + * + * https://gist.github.com/benjchristensen/a962c4bfa8adae286daf + * + * It is here just for demonstration purpose. It is not even being called anywhere on this app + */ + private Subscription startScanOperatorFibonacciTest() { - // This is the seed which will be used in the scan operator. This will be used the first time accumulator function is - // called. - Pair seed = new Pair<>(0, 0); + // This is the seed which will be used in the scan operator. This will be used the first time accumulator function is + // called. + Pair seed = new Pair<>(0, 0); - return emitItems(15, false, true) + return emitItems(15, false, true) - // This example ignores source emission, but instead only processes result of the function - .scan(seed, (accumulator, source) -> { - Log.v(TAG, "scan() - source: " + source + " - accumulator: " + accumulator); + // This example ignores source emission, but instead only processes result of the function + .scan(seed, (accumulator, source) -> { + Log.v(TAG, "scan() - source: " + source + " - accumulator: " + accumulator); - Integer f1 = accumulator.first; - Integer f2 = accumulator.second; + Integer f1 = accumulator.first; + Integer f2 = accumulator.second; - // The first time this accumulator function is called, result will contain a pair with zero value for both first - // and second attributes (this is due the seed parameter we provided in the first scan() parameter. On this case, - // assign zero and one values to f1 and f2 variables. - f1 = f1 == 0 ? 0 : f1; - f2 = f2 == 0 ? 1 : f2; + // The first time this accumulator function is called, result will contain a pair with zero value for both first + // and second attributes (this is due the seed parameter we provided in the first scan() parameter. On this case, + // assign zero and one values to f1 and f2 variables. + f1 = f1 == 0 ? 0 : f1; + f2 = f2 == 0 ? 1 : f2; - // Now sum both values... - int fn = f1 + f2; + // Now sum both values... + int fn = f1 + f2; - // ...and re-assign new values to the return attribute. - accumulator = new Pair<>(f2, fn); + // ...and re-assign new values to the return attribute. + accumulator = new Pair<>(f2, fn); - return accumulator; - }) + return accumulator; + }) - // Just for log purpose - .compose(showDebugMessages("scan")) + // Just for log purpose + .compose(showDebugMessages("scan")) - .filter((number) -> number.first != 0) + .filter((number) -> number.first != 0) - // Just for log purpose - .compose(showDebugMessages("filter")) + // Just for log purpose + .compose(showDebugMessages("filter")) - // Sum both f1 and f2, since we are implementing a fibonacci sequence. - // This is what will be propagated downstream. - .map((number) -> number.first + number.second) + // Sum both f1 and f2, since we are implementing a fibonacci sequence. + // This is what will be propagated downstream. + .map((number) -> number.first + number.second) - // Just for log purpose - .compose(showDebugMessages("map")) + // Just for log purpose + .compose(showDebugMessages("map")) - // Also, starts the sequence with these values (this is due the first three numbers of the fibonacci sequence) - .startWith(0, 1, 1) + // Also, starts the sequence with these values (this is due the first three numbers of the fibonacci sequence) + .startWith(0, 1, 1) - // Just for log purpose - .compose(showDebugMessages("startWith")) + // Just for log purpose + .compose(showDebugMessages("startWith")) - // Now, apply on which thread observable will run and also on which one it will be observed. - .compose(applySchedulers()) + // Now, apply on which thread observable will run and also on which one it will be observed. + .compose(applySchedulers()) - // Finally subscribe it. - .subscribe(resultSubscriber(tvResult)); - } + // Finally subscribe it. + .subscribe(resultSubscriber(tvResult)); + } } diff --git a/app/src/main/java/com/motondon/rxjavademoapp/view/parallelization/ParallelizationExampleActivity.java b/app/src/main/java/com/motondon/rxjavademoapp/view/parallelization/ParallelizationExampleActivity.java index 9dd3f82..d85eab0 100644 --- a/app/src/main/java/com/motondon/rxjavademoapp/view/parallelization/ParallelizationExampleActivity.java +++ b/app/src/main/java/com/motondon/rxjavademoapp/view/parallelization/ParallelizationExampleActivity.java @@ -14,11 +14,12 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; - +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnItemSelected; import com.motondon.rxjavademoapp.R; import com.motondon.rxjavademoapp.view.adapter.SimpleStringAdapter; import com.motondon.rxjavademoapp.view.base.BaseActivity; - import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; @@ -27,10 +28,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnItemSelected; import rx.Observable; import rx.Scheduler; import rx.Subscriber; @@ -40,442 +37,423 @@ /** * Most examples in this activity were based on the following articles: - * - http://tomstechnicalblog.blogspot.com.br/2015/11/rxjava-achieving-parallelization.html - * - http://tomstechnicalblog.blogspot.com.br/2016/02/rxjava-maximizing-parallelization.html + * - http://tomstechnicalblog.blogspot.com.br/2015/11/rxjava-achieving-parallelization.html + * - http://tomstechnicalblog.blogspot.com.br/2016/02/rxjava-maximizing-parallelization.html * * This is a good SO question that might be useful when dealing with schedulers: - * - http://stackoverflow.com/questions/31276164/rxjava-schedulers-use-cases - * - * Please, visit them in order to get into details. + * - http://stackoverflow.com/questions/31276164/rxjava-schedulers-use-cases * + * Please, visit them in order to get into details. */ public class ParallelizationExampleActivity extends BaseActivity { - /** - * This is a helper class that is used to fill the spinner up with pairs of test name and a related method name. - * - */ - class SpinnerOptionAndMethodName extends Pair { + /** + * This is a helper class that is used to fill the spinner up with pairs of test name and a related method name. + */ + class SpinnerOptionAndMethodName extends Pair { - public SpinnerOptionAndMethodName(String first, String second) { - super(first, second); - } - - @Override - public String toString() { - return (String) first; - } + public SpinnerOptionAndMethodName(String first, String second) { + super(first, second); } - private static final String TAG = ParallelizationExampleActivity.class.getSimpleName(); - private static final Integer MAX_CONCURRENT = 20; - - @BindView(R.id.s_test_options) Spinner sTestOptions; - @BindView(R.id.my_list) RecyclerView listResult; - @BindView(R.id.tv_status) TextView tvStatus; - @BindView(R.id.btn_start_test) - Button btnStartTest; - - private SimpleStringAdapter mSimpleStringAdapter; - - // Holds the method name related to the test user chosen in the spinner. It will be used via reflection to call the method instance. - private String currentTestMethodName; - - private ColorStateList defaultTextViewResultColor; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_parallelization_example); - ButterKnife.bind(this); - - // Fill spinner view up with all available test names and related method names. We will use reflection to call a method based on the user choice - List testOptions = new ArrayList<>(); - testOptions.add(new SpinnerOptionAndMethodName("Another Single Thread", "startAnotherSingleThreadTest")); - testOptions.add(new SpinnerOptionAndMethodName("Schedulers.newThread", "startSchedulersNewThreadTest")); - testOptions.add(new SpinnerOptionAndMethodName("Too many threads", "startTooManyThreadsTest")); - testOptions.add(new SpinnerOptionAndMethodName("Schedulers.computation", "startSchedulersComputationTest")); - testOptions.add(new SpinnerOptionAndMethodName("newThread with maxConcurrency", "startNewThreadWithMaxConcurrencyTest")); - testOptions.add(new SpinnerOptionAndMethodName("IO Thread with maxConcurrency", "startIoThreadWithMaxConcurrencyTest")); - testOptions.add(new SpinnerOptionAndMethodName("Get number of available processors", "startAvailableProcessorsTest")); - testOptions.add(new SpinnerOptionAndMethodName("Round-Robin (w/groupBy)", "startRoundRobinWithGroupByOperatorTest")); - - ArrayAdapter adapter = new ArrayAdapter<>( - this, android.R.layout.simple_spinner_item, testOptions); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - sTestOptions.setAdapter(adapter); - - // Set the default method name (the first option). - currentTestMethodName = "startAnotherSingleThreadTest"; - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(getIntent().getStringExtra("TITLE")); - } - - btnStartTest.setOnClickListener(view -> onButtonClick()); - - listResult.setLayoutManager(new LinearLayoutManager(this)); - mSimpleStringAdapter = new SimpleStringAdapter(this); - listResult.setAdapter(mSimpleStringAdapter); - - defaultTextViewResultColor = tvStatus.getTextColors(); + public String toString() { + return (String) first; } - - @OnItemSelected(R.id.s_test_options) - public void spinnerTestOptionsItemSelected(Spinner spinner, int position) { - SpinnerOptionAndMethodName testItem = (SpinnerOptionAndMethodName) spinner.getAdapter().getItem(position); - - currentTestMethodName = (String) testItem.second; + } + + private static final String TAG = ParallelizationExampleActivity.class.getSimpleName(); + private static final Integer MAX_CONCURRENT = 20; + + @BindView(R.id.s_test_options) + Spinner sTestOptions; + @BindView(R.id.my_list) + RecyclerView listResult; + @BindView(R.id.tv_status) + TextView tvStatus; + @BindView(R.id.btn_start_test) + Button btnStartTest; + + private SimpleStringAdapter mSimpleStringAdapter; + + // Holds the method name related to the test user chosen in the spinner. It will be used via reflection to call the method instance. + private String currentTestMethodName; + + private ColorStateList defaultTextViewResultColor; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_parallelization_example); + ButterKnife.bind(this); + + // Fill spinner view up with all available test names and related method names. We will use reflection to call a method based on the user choice + List testOptions = new ArrayList<>(); + testOptions.add( + new SpinnerOptionAndMethodName("Another Single Thread", "startAnotherSingleThreadTest")); + testOptions.add( + new SpinnerOptionAndMethodName("Schedulers.newThread", "startSchedulersNewThreadTest")); + testOptions.add(new SpinnerOptionAndMethodName("Too many threads", "startTooManyThreadsTest")); + testOptions.add( + new SpinnerOptionAndMethodName("Schedulers.computation", "startSchedulersComputationTest")); + testOptions.add(new SpinnerOptionAndMethodName("newThread with maxConcurrency", + "startNewThreadWithMaxConcurrencyTest")); + testOptions.add(new SpinnerOptionAndMethodName("IO Thread with maxConcurrency", + "startIoThreadWithMaxConcurrencyTest")); + testOptions.add(new SpinnerOptionAndMethodName("Get number of available processors", + "startAvailableProcessorsTest")); + testOptions.add(new SpinnerOptionAndMethodName("Round-Robin (w/groupBy)", + "startRoundRobinWithGroupByOperatorTest")); + + ArrayAdapter adapter = + new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, testOptions); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + sTestOptions.setAdapter(adapter); + + // Set the default method name (the first option). + currentTestMethodName = "startAnotherSingleThreadTest"; + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getIntent().getStringExtra("TITLE")); } - public void onButtonClick() { - if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { - Log.v(TAG, "onTakeExampleButtonClick()"); + btnStartTest.setOnClickListener(view -> onButtonClick()); - tvStatus.setTextColor(defaultTextViewResultColor); - tvStatus.setText("Running test..."); - mSimpleStringAdapter.clear(); + listResult.setLayoutManager(new LinearLayoutManager(this)); + mSimpleStringAdapter = new SimpleStringAdapter(this); + listResult.setAdapter(mSimpleStringAdapter); - try { + defaultTextViewResultColor = tvStatus.getTextColors(); + } - Method m = this.getClass().getDeclaredMethod(currentTestMethodName); - subscription = (Subscription) m.invoke(this); + @OnItemSelected(R.id.s_test_options) + public void spinnerTestOptionsItemSelected(Spinner spinner, int position) { + SpinnerOptionAndMethodName testItem = + (SpinnerOptionAndMethodName) spinner.getAdapter().getItem(position); - } catch (NoSuchMethodException e) { - Log.e(TAG, "onTakeExampleButtonClick() - NoSuchMethodException: " + e.getMessage()); - } catch (InvocationTargetException e) { - Log.e(TAG, "onTakeExampleButtonClick() - InvocationTargetException: " + e.getMessage()); - } catch (IllegalAccessException e) { - Log.d(TAG, "onTakeExampleButtonClick() - IllegalAccessException: " + e.getMessage()); - } + currentTestMethodName = (String) testItem.second; + } - } else { - Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); - } - } + public void onButtonClick() { + if (subscription == null || (subscription != null && subscription.isUnsubscribed())) { + Log.v(TAG, "onTakeExampleButtonClick()"); - /** - * This example demonstrates how to ask our observable to be executed in another thread than that one subscribe method was - * called. To do so, we just call subscriberOn() and use a scheduler on it (this example uses Schedulers.IO). We can also - * create our own Scheduler implementation, but this is out of scope of this example. - * - * Also we are using observerOn() operator to instruct our observable to emit notifications to its observable using Android - * mainThread. - * - * xxx - * - * When using subscribeOn, we decide on what Scheduler the Observable.create (or any static method which implicitly calls - * it) is executed. - * - * observeOn controls, on the other hand, the other side of the pipeline, instructing Observable to perform its emission - * and notifications on a specific scheduler. - * - * Important to note that when calling subscriberOn, no matter at what point in the chain of operators it is called, it - * designates which thread the Observable will begin operating on. - * - * Note this will not make emissions to happen in parallel, but all of them will happen in another thread, but synchronously - * on that thread. - * - * - * From the docs: - * - * "By default, an Observable and the chain of operators that you apply to it will do its work, and will notify its observers, - * on the same thread on which its Subscribe method is called. The SubscribeOn operator changes this behavior by specifying a - * different Scheduler on which the Observable should operate. The ObserveOn operator specifies a different Scheduler that - * the Observable will use to send notifications to its observers." - * - * xxx - * - * One last note: Schedulers.newThread is not recommended to use for intensive computational work (exactly what we are doing in - * our heavyDataProcessing method) since computational work is expected to use CUP intensively and Schedulers.newThread teorerically - * can create unlimited threads, which might decrease the performance. But, since this example is only intended to demonstrate - * how to run an observable in another thread than the mainThread, we will use Schedulers.newThread to show each emission will be - * processed in a different thread. - * - * @return - */ - private Subscription startAnotherSingleThreadTest() { - - return Observable.range(0, 10) - .map(number -> heavyDataProcessing(number, 100, 500)) - .compose(this.showDebugMessages("map")) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } - - /** - * As pointed out by Thomas Nield in the links above, by just using subscribeOn we are not achieving parallelization, - * but just instructing our Observable to be executed in another single thread (this was demonstrated in the - * startAnotherSingleThreadTest example. - * - * Now, we will make real parallelization. To do so, we can use flatMap() operator and call subscribeOn() for each new - * observable it returns. - * - * Depends on how many threads we are creating, this might not be a good approach, since newThread will (in theory) - * create unlimited threads an will throw an exception if it cannot allocate resources to create a new thread. - * - * The result for this method is that each emission will be executed in a different thread in parallel (ok, not every - * emission is in parallel, since it depends on the number of cores. On the device I used to test it, I had two cores - * and so, we have some (little) real concurrence). - * - * We will see later how to limit the number of threads based on the number of available processors. - * - * @return - */ - private Subscription startSchedulersNewThreadTest() { - - return Observable.range(0, 10) - .flatMap(number -> Observable - .just(number) - .subscribeOn(Schedulers.newThread()) - .map(n -> heavyDataProcessing(n, 1000, 5000) - )) - .compose(this.showDebugMessages("flatMap")) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } - - /** - * This example is similar to the previous one (startSchedulersNewThreadTest), but instead of create only - * 10 threads, it will create 10000! This is to demonstrate that Schedulers.newThread will try create new - * threads for each tasks until it can allocate resources for it. Once it cannot create a new thread, it will - * throw an exception. - * - * @return - */ - private Subscription startTooManyThreadsTest() { - - return Observable.range(0, 10000) - .flatMap(number -> Observable - .just(number) - .subscribeOn(Schedulers.newThread()) - .map(n -> heavyDataProcessing(n, 100, 500)) - ) - .compose(this.showDebugMessages("flatMap")) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } + tvStatus.setTextColor(defaultTextViewResultColor); + tvStatus.setText("Running test..."); + mSimpleStringAdapter.clear(); - /** - * This example demonstrates how to run parallel computation threads. - * - * Due the computation scheduler nature, even if we request a huge number of tasks to run in parallel, it will - * limit the number of parallel threads based on the the number of processors. - * - * Ex.: In a dual core system, we will have up to 2 simultaneously threads. - * - * @return - */ - private Subscription startSchedulersComputationTest() { - - return Observable.range(0, 10) - .flatMap(number -> Observable - .just(number) - // Schedulers.computation() will limit the number of simultaneously threads based on the number - // of available processors. - .subscribeOn(Schedulers.computation()) - .map(n -> heavyDataProcessing(n, 3000, 5000)) - ) - .compose(this.showDebugMessages("flatMap")) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } - - /** - * This test will use a flatMap maxConcurrent parameter to MAX_CONCURRENT (which we set its value to 20) in order to - * limit up to 20 simultaneously threads. This is a good approach to use with Schedulers.newThread and Schedulers.IO, - * since both schedulers in theory could create unlimited number of threads. - * - * We can see it in the result GUI RecycleBin list, which will print "blocks" of 20 items each 8-9 seconds. In the end - * we can see that, although only 20 thread were executed simultaneously, all of the 60 tasks used in the example were - * executed using different threads (due the Scheduleer.newThread which always creates a new thread for each task). - * - * @return - */ - private Subscription startNewThreadWithMaxConcurrencyTest() { - - return Observable.range(0, 60) - .flatMap(number -> Observable - .just(number) - .subscribeOn(Schedulers.newThread()) - .map(n -> heavyDataProcessing(n, 4000, 6000)), - // Even when requesting tasks to be executed using a new thread, system will limit the number of simultaneously - // treads up to 20, due this flatmap maxConcurrent parameter. - MAX_CONCURRENT - ) - .compose(this.showDebugMessages("flatMap")) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } + try { - /** - * This test will also use MAX_CONCURRENT (which is set to value of 20) in order to limit up to 20 simultaneously - * threads, but it differs from the previous one (startNewThreadWithMaxConcurrencyTest) in a way it uses - * Schedulers.IO which uses internally a thread pool (that grows as needed and in our case it will grow up to 20 - * threads). So, whenever a thread finishes its job, that thread goes back to the thread pool in order to be reused - * when needed. - * - * We can see it in the result GUI RecycleBin list, which will print "blocks" of 20 items each 8-9 seconds. In the end - * we can see that only 20 threads were created to process all of the 60 tasks (due the Scheduleers.IO that keeps a - * thread pool internally). - * - * @return - */ - private Subscription startIoThreadWithMaxConcurrencyTest() { - - return Observable.range(0, 60) - .flatMap(number -> Observable - .just(number) - .subscribeOn(Schedulers.io()) - .map(n -> heavyDataProcessing(n, 3000, 4000)), - MAX_CONCURRENT - ) - .compose(this.showDebugMessages("flatMap")) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); + Method m = this.getClass().getDeclaredMethod(currentTestMethodName); + subscription = (Subscription) m.invoke(this); + } catch (NoSuchMethodException e) { + Log.e(TAG, "onTakeExampleButtonClick() - NoSuchMethodException: " + e.getMessage()); + } catch (InvocationTargetException e) { + Log.e(TAG, "onTakeExampleButtonClick() - InvocationTargetException: " + e.getMessage()); + } catch (IllegalAccessException e) { + Log.d(TAG, "onTakeExampleButtonClick() - IllegalAccessException: " + e.getMessage()); + } + } else { + Toast.makeText(getApplicationContext(), "Test is already running", Toast.LENGTH_SHORT).show(); } - - /** - * This example will get the number of available processors and create an executor based on it. Then, - * it will create a Scheduler which will use that executor. Later that scheduler will be use in the - * subscribeOn() operator. - * - * By creating a scheduler from an executor, it gives us more control over how many threads we want - * to run in parallel. In our case, we will use number of processors + 1 which seems to be a good - * approach to not to decrease the system performance. - * - * When comparing this test with startSchedulersComputationTest(), we can note this one runs faster. - * I do not know how Schedulers.computation works internally, but it seems it is not so optimized as - * we expect. Maybe there is a plausible reason for that. In the Thomas Nield's maximizin - * parallelization post, there is a comment from @Kaloyan Roussev which he gives a reasonable try - * about it. Anyway, it needs further investigation. - * - * @return - */ - private Subscription startAvailableProcessorsTest() { - - // Get the number of available processors on the device and create a scheduler based on it. - int numberOfThreads = Runtime.getRuntime().availableProcessors() + 1; - final ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); - final Scheduler scheduler = Schedulers.from(executor); - - return Observable.range(0, 40) - .flatMap(number -> Observable - .just(number) - // Since we are using a scheduler from an executor, we can control how many threads - // will really run in parallel. - .subscribeOn(scheduler) - .map(n -> heavyDataProcessing(n, 3000, 5000)) - ) - .finallyDo(() -> executor.shutdown()) - .compose(this.showDebugMessages("flatMap")) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } - - /** - * This is a very interesting example. I found it first in the Thomas Nield article in the question - * section in a question by David Karnok (http://tomstechnicalblog.blogspot.com.br/2015/11/rxjava-achieving-parallelization.html) - * and then later Thomas created a post about it (http://tomstechnicalblog.blogspot.com.br/2016/02/rxjava-maximizing-parallelization.html). - * - * Depends on what kind of processing we are doing in a thread, creating Observable multiple times can be very expensive (imagine a - * situation where we have 100000 items to be processed!). So it is possible to break up the emissions into batches by using groupBy - * operator and flatmap the groups. This example breaks up emissions in [number of available cores + 1] GroupObservables, and each - * one will run in a different thread. - * - * I strongly recommend you to take a look in the links mentioned above. They are very very useful! - * - * @return - */ - private Subscription startRoundRobinWithGroupByOperatorTest() { - - final AtomicInteger n = new AtomicInteger(0); - - // Create a scheduler based on the number of available cores + 1. - final int numberOfThreads = Runtime.getRuntime().availableProcessors() + 1; - ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); - final Scheduler scheduler = Schedulers.from(executor); - - return Observable.range(0, 40) - .groupBy(number -> { - // Just get AtomicInteger "n" variable value here for the log purpose, since after getAndIncrement() method call returns, its value is - // incremented, but we want to log it prior incrementation. - Integer i = n.getAndIncrement(); - Integer modulus = i % numberOfThreads; - Log.v(TAG, "groupBy - Data: " + number + " - n: " + n + " - n.getAndIncrement() % " + numberOfThreads + ": " + modulus); - - // Since we are incrementing this AtomicInteger for every emitted item, they will be grouped in batches - // of numbers multiple of numberOfThreads + 1. - return modulus; - }) - - // groupBy operator will group emissions into observables based on the result of its function. So, for each group will - // be created (or reused if it is already created) an observable on which items will be emitted on. Then, all we have to do is - // to flatmap it and use GroupedObservable to map the emitted items and call our heavy processing method. - .flatMap(num -> num - .observeOn(scheduler) - // The groupBy operator requires the returned GroupedObservable to be subscribed, otherwise it won't request more data. - // So we need to work upon the groupObservable in order to be able to get all items emitted on each group. - .map(nn -> heavyDataProcessing(nn, 400, 500)) - ) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber()); - } - - /** - * This is a helper method which just sleeps for a while in order to simulate a heavy data processing. - * - * @param number - * @param minSleepTime - * @param maxSleepTime - * @return - */ - private Integer heavyDataProcessing(Integer number, Integer minSleepTime, Integer maxSleepTime) { - try { - - Integer timeToSleep = new Random().nextInt(maxSleepTime - minSleepTime) + minSleepTime; - final String msg = "Processing data " + number + " on thread: " + Thread.currentThread().getName() + " - Sleeping for " + timeToSleep + "ms..."; - Log.v(TAG, msg); - - final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); - w.schedule(() -> { - mSimpleStringAdapter.addString(msg); - listResult.scrollToPosition(mSimpleStringAdapter.getItemCount()-1); - }); - - Thread.sleep(timeToSleep); - - return number; - - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - @NonNull - private Subscriber resultSubscriber() { - return new Subscriber() { - - @Override - public void onCompleted() { - Log.v(TAG, "subscribe.onCompleted"); - tvStatus.setText("Test executed successfully."); - tvStatus.setTextColor(Color.parseColor("#99cc00")); - } - - @Override - public void onError(Throwable e) { - Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); - tvStatus.setTextColor(Color.parseColor("#d50000")); - tvStatus.setText("Test finished with errors: " + e.getMessage()); - } - - @Override - public void onNext(Integer number) { - Log.v(TAG, "subscribe.onNext" + " " + number); - } - }; + } + + /** + * This example demonstrates how to ask our observable to be executed in another thread than that one subscribe method was + * called. To do so, we just call subscriberOn() and use a scheduler on it (this example uses Schedulers.IO). We can also + * create our own Scheduler implementation, but this is out of scope of this example. + * + * Also we are using observerOn() operator to instruct our observable to emit notifications to its observable using Android + * mainThread. + * + * xxx + * + * When using subscribeOn, we decide on what Scheduler the Observable.create (or any static method which implicitly calls + * it) is executed. + * + * observeOn controls, on the other hand, the other side of the pipeline, instructing Observable to perform its emission + * and notifications on a specific scheduler. + * + * Important to note that when calling subscriberOn, no matter at what point in the chain of operators it is called, it + * designates which thread the Observable will begin operating on. + * + * Note this will not make emissions to happen in parallel, but all of them will happen in another thread, but synchronously + * on that thread. + * + * + * From the docs: + * + * "By default, an Observable and the chain of operators that you apply to it will do its work, and will notify its observers, + * on the same thread on which its Subscribe method is called. The SubscribeOn operator changes this behavior by specifying a + * different Scheduler on which the Observable should operate. The ObserveOn operator specifies a different Scheduler that + * the Observable will use to send notifications to its observers." + * + * xxx + * + * One last note: Schedulers.newThread is not recommended to use for intensive computational work (exactly what we are doing in + * our heavyDataProcessing method) since computational work is expected to use CUP intensively and Schedulers.newThread teorerically + * can create unlimited threads, which might decrease the performance. But, since this example is only intended to demonstrate + * how to run an observable in another thread than the mainThread, we will use Schedulers.newThread to show each emission will be + * processed in a different thread. + */ + private Subscription startAnotherSingleThreadTest() { + + return Observable.range(0, 10) + .map(number -> heavyDataProcessing(number, 100, 500)) + .compose(this.showDebugMessages("map")) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * As pointed out by Thomas Nield in the links above, by just using subscribeOn we are not achieving parallelization, + * but just instructing our Observable to be executed in another single thread (this was demonstrated in the + * startAnotherSingleThreadTest example. + * + * Now, we will make real parallelization. To do so, we can use flatMap() operator and call subscribeOn() for each new + * observable it returns. + * + * Depends on how many threads we are creating, this might not be a good approach, since newThread will (in theory) + * create unlimited threads an will throw an exception if it cannot allocate resources to create a new thread. + * + * The result for this method is that each emission will be executed in a different thread in parallel (ok, not every + * emission is in parallel, since it depends on the number of cores. On the device I used to test it, I had two cores + * and so, we have some (little) real concurrence). + * + * We will see later how to limit the number of threads based on the number of available processors. + */ + private Subscription startSchedulersNewThreadTest() { + + return Observable.range(0, 10) + .flatMap(number -> Observable.just(number) + .subscribeOn(Schedulers.newThread()) + .map(n -> heavyDataProcessing(n, 1000, 5000))) + .compose(this.showDebugMessages("flatMap")) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This example is similar to the previous one (startSchedulersNewThreadTest), but instead of create only + * 10 threads, it will create 10000! This is to demonstrate that Schedulers.newThread will try create new + * threads for each tasks until it can allocate resources for it. Once it cannot create a new thread, it will + * throw an exception. + */ + private Subscription startTooManyThreadsTest() { + + return Observable.range(0, 10000) + .flatMap(number -> Observable.just(number) + .subscribeOn(Schedulers.newThread()) + .map(n -> heavyDataProcessing(n, 100, 500))) + .compose(this.showDebugMessages("flatMap")) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This example demonstrates how to run parallel computation threads. + * + * Due the computation scheduler nature, even if we request a huge number of tasks to run in parallel, it will + * limit the number of parallel threads based on the the number of processors. + * + * Ex.: In a dual core system, we will have up to 2 simultaneously threads. + */ + private Subscription startSchedulersComputationTest() { + + return Observable.range(0, 10) + .flatMap(number -> Observable.just(number) + // Schedulers.computation() will limit the number of simultaneously threads based on the number + // of available processors. + .subscribeOn(Schedulers.computation()).map(n -> heavyDataProcessing(n, 3000, 5000))) + .compose(this.showDebugMessages("flatMap")) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This test will use a flatMap maxConcurrent parameter to MAX_CONCURRENT (which we set its value to 20) in order to + * limit up to 20 simultaneously threads. This is a good approach to use with Schedulers.newThread and Schedulers.IO, + * since both schedulers in theory could create unlimited number of threads. + * + * We can see it in the result GUI RecycleBin list, which will print "blocks" of 20 items each 8-9 seconds. In the end + * we can see that, although only 20 thread were executed simultaneously, all of the 60 tasks used in the example were + * executed using different threads (due the Scheduleer.newThread which always creates a new thread for each task). + */ + private Subscription startNewThreadWithMaxConcurrencyTest() { + + return Observable.range(0, 60) + .flatMap(number -> Observable.just(number) + .subscribeOn(Schedulers.newThread()) + .map(n -> heavyDataProcessing(n, 4000, 6000)), + // Even when requesting tasks to be executed using a new thread, system will limit the number of simultaneously + // treads up to 20, due this flatmap maxConcurrent parameter. + MAX_CONCURRENT) + .compose(this.showDebugMessages("flatMap")) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This test will also use MAX_CONCURRENT (which is set to value of 20) in order to limit up to 20 simultaneously + * threads, but it differs from the previous one (startNewThreadWithMaxConcurrencyTest) in a way it uses + * Schedulers.IO which uses internally a thread pool (that grows as needed and in our case it will grow up to 20 + * threads). So, whenever a thread finishes its job, that thread goes back to the thread pool in order to be reused + * when needed. + * + * We can see it in the result GUI RecycleBin list, which will print "blocks" of 20 items each 8-9 seconds. In the end + * we can see that only 20 threads were created to process all of the 60 tasks (due the Scheduleers.IO that keeps a + * thread pool internally). + */ + private Subscription startIoThreadWithMaxConcurrencyTest() { + + return Observable.range(0, 60) + .flatMap(number -> Observable.just(number) + .subscribeOn(Schedulers.io()) + .map(n -> heavyDataProcessing(n, 3000, 4000)), MAX_CONCURRENT) + .compose(this.showDebugMessages("flatMap")) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This example will get the number of available processors and create an executor based on it. Then, + * it will create a Scheduler which will use that executor. Later that scheduler will be use in the + * subscribeOn() operator. + * + * By creating a scheduler from an executor, it gives us more control over how many threads we want + * to run in parallel. In our case, we will use number of processors + 1 which seems to be a good + * approach to not to decrease the system performance. + * + * When comparing this test with startSchedulersComputationTest(), we can note this one runs faster. + * I do not know how Schedulers.computation works internally, but it seems it is not so optimized as + * we expect. Maybe there is a plausible reason for that. In the Thomas Nield's maximizin + * parallelization post, there is a comment from @Kaloyan Roussev which he gives a reasonable try + * about it. Anyway, it needs further investigation. + */ + private Subscription startAvailableProcessorsTest() { + + // Get the number of available processors on the device and create a scheduler based on it. + int numberOfThreads = Runtime.getRuntime().availableProcessors() + 1; + final ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); + final Scheduler scheduler = Schedulers.from(executor); + + return Observable.range(0, 40) + .flatMap(number -> Observable.just(number) + // Since we are using a scheduler from an executor, we can control how many threads + // will really run in parallel. + .subscribeOn(scheduler).map(n -> heavyDataProcessing(n, 3000, 5000))) + .finallyDo(() -> executor.shutdown()) + .compose(this.showDebugMessages("flatMap")) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This is a very interesting example. I found it first in the Thomas Nield article in the question + * section in a question by David Karnok (http://tomstechnicalblog.blogspot.com.br/2015/11/rxjava-achieving-parallelization.html) + * and then later Thomas created a post about it (http://tomstechnicalblog.blogspot.com.br/2016/02/rxjava-maximizing-parallelization.html). + * + * Depends on what kind of processing we are doing in a thread, creating Observable multiple times can be very expensive (imagine a + * situation where we have 100000 items to be processed!). So it is possible to break up the emissions into batches by using groupBy + * operator and flatmap the groups. This example breaks up emissions in [number of available cores + 1] GroupObservables, and each + * one will run in a different thread. + * + * I strongly recommend you to take a look in the links mentioned above. They are very very useful! + */ + private Subscription startRoundRobinWithGroupByOperatorTest() { + + final AtomicInteger n = new AtomicInteger(0); + + // Create a scheduler based on the number of available cores + 1. + final int numberOfThreads = Runtime.getRuntime().availableProcessors() + 1; + ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); + final Scheduler scheduler = Schedulers.from(executor); + + return Observable.range(0, 40) + .groupBy(number -> { + // Just get AtomicInteger "n" variable value here for the log purpose, since after getAndIncrement() method call returns, its value is + // incremented, but we want to log it prior incrementation. + Integer i = n.getAndIncrement(); + Integer modulus = i % numberOfThreads; + Log.v(TAG, "groupBy - Data: " + + number + + " - n: " + + n + + " - n.getAndIncrement() % " + + numberOfThreads + + ": " + + modulus); + + // Since we are incrementing this AtomicInteger for every emitted item, they will be grouped in batches + // of numbers multiple of numberOfThreads + 1. + return modulus; + }) + + // groupBy operator will group emissions into observables based on the result of its function. So, for each group will + // be created (or reused if it is already created) an observable on which items will be emitted on. Then, all we have to do is + // to flatmap it and use GroupedObservable to map the emitted items and call our heavy processing method. + .flatMap(num -> num.observeOn(scheduler) + // The groupBy operator requires the returned GroupedObservable to be subscribed, otherwise it won't request more data. + // So we need to work upon the groupObservable in order to be able to get all items emitted on each group. + .map(nn -> heavyDataProcessing(nn, 400, 500))) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber()); + } + + /** + * This is a helper method which just sleeps for a while in order to simulate a heavy data processing. + */ + private Integer heavyDataProcessing(Integer number, Integer minSleepTime, Integer maxSleepTime) { + try { + + Integer timeToSleep = new Random().nextInt(maxSleepTime - minSleepTime) + minSleepTime; + final String msg = "Processing data " + + number + + " on thread: " + + Thread.currentThread().getName() + + " - Sleeping for " + + timeToSleep + + "ms..."; + Log.v(TAG, msg); + + final Scheduler.Worker w = AndroidSchedulers.mainThread().createWorker(); + w.schedule(() -> { + mSimpleStringAdapter.addString(msg); + listResult.scrollToPosition(mSimpleStringAdapter.getItemCount() - 1); + }); + + Thread.sleep(timeToSleep); + + return number; + } catch (InterruptedException e) { + throw new RuntimeException(e); } + } + + @NonNull + private Subscriber resultSubscriber() { + return new Subscriber() { + + @Override + public void onCompleted() { + Log.v(TAG, "subscribe.onCompleted"); + tvStatus.setText("Test executed successfully."); + tvStatus.setTextColor(Color.parseColor("#99cc00")); + } + + @Override + public void onError(Throwable e) { + Log.v(TAG, "subscribe.doOnError: " + e.getMessage()); + tvStatus.setTextColor(Color.parseColor("#d50000")); + tvStatus.setText("Test finished with errors: " + e.getMessage()); + } + + @Override + public void onNext(Integer number) { + Log.v(TAG, "subscribe.onNext" + " " + number); + } + }; + } } diff --git a/build.gradle b/build.gradle index e2fac97..f360814 100644 --- a/build.gradle +++ b/build.gradle @@ -1,25 +1,23 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - repositories { - jcenter() - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.1.0' - - classpath 'me.tatarka:gradle-retrolambda:3.3.0' - classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2' - } + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.2.0-alpha14' + classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2' + } } allprojects { - repositories { - jcenter() - google() - } + repositories { + jcenter() + google() + } } task clean(type: Delete) { - delete rootProject.buildDir + delete rootProject.buildDir } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c5050ab..7d3178f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Apr 03 10:09:28 BRT 2018 +#Tue May 15 04:41:29 EET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip