Apologies in advance if I lack a basic understanding of how to use RxJava2, because this seems to me something that should be quite fundamental. I've wracked my brains with unsuccessful Google searches, so welcome any resource recommendations. I've opted to use a 'sanitized' representation of my workaround code for the sake of clarity.
Problem description
I have an RxJava2 function asyncCallForList() that returns a Maybe<Arraylist<CustomClass>>. Each CustomClass object in this list only has a few basic fields populated (e.g. the source database only contains a unique identifier and a title string for each item).
The full data required for each item is in another database location, which is retrieved using another function asyncCallForItem(uid), which returns a Maybe<CustomClass> based on the unique identifier, where the encapsulated CustomClass has all the required data. This function is to be called for each item in the list returned by asyncCallForList().
The desired functionality is to update my UI once all the objects in the list have been populated.
Workaround #1
It is easy enough to loop through the resulting array list in the doOnSuccess() attached to the initial Maybe<Arraylist<CustomClass>>, then update my UI in the doOnSuccess() on the Maybe<CustomClass> returned by the subsequent asynchronous calls. This is not an acceptable workaround as there will be an unknown number of UI updates being made (the initial list returned could have any amount of items) and will hurt performance.
Workaround #2
This gets the desired outcome but feels like the wrong way to go about it - I suspect there is a more elegant RxJava2 solution. Basically, I create a custom Observable in which loop through the items in the list and get the full data for each. However, rather than update the UI each time I populate a CustomClass item, I increase a counter, then check if the counter exceeds or equals the initial list size. When this condition is met I call the onComplete() method for the observable's emitter and update the UI there.
private void fetchRemoteDataAndUpdateUi() {
//Counter reset to zero before any asynchronous calls are made.
int count = 0;
Maybe<ArrayList<CustomClass>> itemList = asyncCallForList();
Consumer<ArrayList<CustomClass>> onListReturnedSuccess;
onListReturnedSuccess = new Consumer<ArrayList<CustomClass >>() {
#Override
public void accept(ArrayList<CustomClass> list) throws Exception {
//Custom observable created here, in which the resulting array list is processed.
listObservable = Observable.create(new ObservableOnSubscribe<CustomClass>() {
#Override
public void subscribe(final ObservableEmitter<CustomClass> e) throws Exception {
for (CustomClass customClass : list) {
final CustomClass thisCustomClass = customClass;
//Call to get full data on list item called here.
asyncCallForItem(customClass.getUid())
.doOnSuccess(new Consumer<CustomClass>() {
#Override
public void accept(CustomClass customClass) throws Exception {
thisCustomClass.update(customClass);
e.onNext(thisCustomClass);
count++;
if (count >= list.size()) {
e.onComplete();
}
}
}).subscribe();
}
}
});
listObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Observer<CustomClass>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(CustomClass customClass) {
//Here I add the populated CustomClass object to an ArrayList field that is utilised by the UI.
listForUi.add(customClass);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
//Here the UI is finally updated once all CustomClass objects have been populated.
updateUi();
}
});
}
};
//Kick everything off.
itemList.doOnSuccess(onListReturnedSuccess).subscribe();
}
flatMap it!
asyncCallForList()
.subscribeOn(Schedulers.io())
.flatMapSingle(list ->
Flowable.fromIterable(list)
.flatMapMaybe(item ->
asyncCallForItem(item.id)
.subscribeOn(Schedulers.io())
.doOnSuccess(response -> {
// copy state from the original item
response.text = item.text;
})
, 1) // number of concurrent item calls
.toList()
)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(successList -> { /* update UI */ }, error -> { /* report error */ });
I am a newbie in reactive world and trying to implement the following scenario with rxjava/rxandroid 2.x.
I have a local data set as ArrayList mItems in Application class. The same data set is synchronized with server and updated every time user opens the app. However before server returns the response, I want to display the local data set in RecycleView backed by adapter. As soon as the response is returned, the adapter should update the list with delta and without disturbing the order in the UI.
So far I have tried this:
public Observable<List<Item>> getItemsObservable() {
Observable<List<Item>> observeApi = itemServiceAPI.getItemsForUser(accountId);
if (mItems != null) {
return Observable.just(mItems).mergeWith(observeApi);
} else {
return observeApi;
}
}
To update the UI, the above method is invoked like this:
Observable<List<Item>> itemsObservable = appContext.getItemsObservable();
itemsObservable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver<List<Item>>() {
#Override
public void onNext(List<Item> Items) {
// Code to update the adapter
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
With this I get the onNext called twice for each local data set and remote data set. How to achieve the desired functionality? Does it need use of filter operators to exclude items?
What's the best approach to achieve this?
You can use 'startWith' operator: it subscribes to different observable first.
appContext.getItemsObservable()
.startWith(localCacheObservable)
.subscribe(adapter::updateData)
Adapter's update data should handle diff calculations.
UPDATE
First of all, why are you using Observable.just(mItems) ??? That's unnecessary.
Your code should look like
itemServiceAPI.getItemsForUser(accountId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DefaultObserver<List<Item>>() {
#Override
public void onNext(List<Item> Items) {
// Code to update the adapter
mAdapter.updateItems(items);
/* method in adapter class
*
* public void updateItems(List<Item> mList) {
this.items.addAll(mList);
notifyDataSetChanged();
}
* */
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
Here, your adapter will be updated in onNext. make sure before calling API, you have to set your adapter with local items.
just like this sample
Observable.fromCallable(() -> okHttp.getDataFromNet(page))
.subscribeOn(Schedulers.io())
.doOnSubscribe(()->{adapter.loadmore})
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<String>() {
#Override
public void onCompleted() {
adapter.stopLoad();
}
#Override
public void onError(Throwable throwable) {
}
#Override
public void onNext(String s) {
Log.e("fail","fail");
adapter.add();
}
});
I find onNextwill be called two times in this case.If i add Log.e() in
onCompleted(),it also will called two times.What is wrong about my code.What is the reason that if a method will be called in Subscriber ,then I add doOnSubscribe() it will be called two times。
//progressbarAdapter extends Recyclerview.Adapter
static final int LOADING_TYPE = -1;
private boolean isLoading = false;
public void loadingMore(){
isLoading = true;
notifyItemInserted(getItemCount());
// size = index +1
}
public void loadingEnd(){
notifyItemRemoved(getItemCount());
isLoading = false;
}
#Override
public int getItemCount() {
return getDataCount() + (isLoading ? 1: 0);
}
#Override
public int getItemViewType(int position) {
if (position == getDataCount() && !isError){
return LOADING_TYPE;
}else {
return getExtItemViewType(position);
}
}
the real reason
when adapter.notify it will trigger scroll methods,so it will be called two times. I am sorry for that I asked a bad question.
please have a look how to use observeOn and subscribeOn. There can only be one subscribeOn for an observable. If you use multiple, the last one wins. Use observeOn to switch threads between pipelines and subscribeOn to specify the thread which will create the observable.
I transformed your given code in order to use it in a test. I only get one onNext-call.
Problem: getting called two times.
This may be because of wrong usage of the observable somewhere else in the code by flatMap or multiple subscriptions to observable. Please provide more context.
Testenvironment: IntelliJ Idea 2017 EAP, io.reactivex:rxjava:1.2.1
#Test
public void name() throws Exception {
Observable<String> doOnSub = Observable.just("1")
.subscribeOn(Schedulers.io())
.doOnSubscribe(() -> {
System.out.println("doOnSub");
})
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation());
TestSubscriber<String> objectTestSubscriber = TestSubscriber.create();
doOnSub.subscribe(objectTestSubscriber);
objectTestSubscriber.awaitTerminalEvent();
objectTestSubscriber.assertValues("1");
}
I have a RecyclerView that is inside a CardView. The CardView has a height of 500dp, but I want to shorten this height if the RecyclerView is smaller.
So I wonder if there is any listener that is called when the RecyclerView has finished laying down its items for the first time, making it possible to set the RecyclerView's height to the CardView's height (if smaller than 500dp).
I also needed to execute code after my recycler view finished inflating all elements. I tried checking in onBindViewHolder in my Adapter, if the position was the last, and then notified the observer. But at that point, the recycler view still was not fully populated.
As RecyclerView implements ViewGroup, this anwser was very helpful. You simply need to add an OnGlobalLayoutListener to the recyclerView:
View recyclerView = findViewById(R.id.myView);
recyclerView
.getViewTreeObserver()
.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// At this point the layout is complete and the
// dimensions of recyclerView and any child views
// are known.
recyclerView
.getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
}
});
Working modification of #andrino anwser.
As #Juancho pointed in comment above. This method is called several times. In this case we want it to be triggered only once.
Create custom listener with instance e.g
private RecyclerViewReadyCallback recyclerViewReadyCallback;
public interface RecyclerViewReadyCallback {
void onLayoutReady();
}
Then set OnGlobalLayoutListener on your RecyclerView
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (recyclerViewReadyCallback != null) {
recyclerViewReadyCallback.onLayoutReady();
}
recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
after that you only need implement custom listener with your code
recyclerViewReadyCallback = new RecyclerViewReadyCallback() {
#Override
public void onLayoutReady() {
//
//here comes your code that will be executed after all items are laid down
//
}
};
If you use Kotlin, then there is a more compact solution.
Sample from here.
This layout listener is usually used to do something after a View is measured, so you typically would need to wait until width and height are greater than 0.
... it can be used by any object that extends View and also be able to access to all its specific functions and properties from the listener.
// define 'afterMeasured' layout listener:
inline fun <T: View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
// using 'afterMeasured' handler:
myRecycler.afterMeasured {
// do the scroll (you can use the RecyclerView functions and properties directly)
// ...
}
The best way that I found to know when has finished laying down the items was using the LinearLayoutManager.
For example:
private RecyclerView recyclerView;
...
recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false){
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
// TODO
}
);
...
I improved the answer of android developer to fix this problem. It's a Kotlin code but should be simple to understand even if you know only Java.
I wrote a subclass of LinearLayoutManager which lets you listen to the onLayoutCompleted() event:
/**
* This class calls [mCallback] (instance of [OnLayoutCompleteCallback]) when all layout
* calculations are complete, e.g. following a call to
* [RecyclerView.Adapter.notifyDataSetChanged()] (or related methods).
*
* In a paginated listing, we will decide if load more needs to be called in the said callback.
*/
class NotifyingLinearLayoutManager(context: Context) : LinearLayoutManager(context, VERTICAL, false) {
var mCallback: OnLayoutCompleteCallback? = null
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
mCallback?.onLayoutComplete()
}
fun isLastItemCompletelyVisible() = findLastCompletelyVisibleItemPosition() == itemCount - 1
interface OnLayoutCompleteCallback {
fun onLayoutComplete()
}
}
Now I set the mCallback like below:
mLayoutManager.mCallback = object : NotifyingLinearLayoutManager.OnLayoutCompleteCallback {
override fun onLayoutComplete() {
// here we know that the view has been updated.
// now you can execute your code here
}
}
Note: what is different from the linked answer is that I use onLayoutComplete() which is only invoked once, as the docs say:
void onLayoutCompleted (RecyclerView.State state)
Called after a full layout calculation is finished. The layout
calculation may include multiple onLayoutChildren(Recycler, State)
calls due to animations or layout measurement but it will include only
one onLayoutCompleted(State) call. This method will be called at the
end of layout(int, int, int, int) call.
This is a good place for the LayoutManager to do some cleanup like
pending scroll position, saved state etc.
I tried this and it worked for me. Here is the Kotlin extension
fun RecyclerView.runWhenReady(action: () -> Unit) {
val globalLayoutListener = object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
action()
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
}
then call it
myRecyclerView.runWhenReady {
// Your action
}
Also in same cases you can use RecyclerView.post() method to run your code after list/grid items are popped up. In my cases it was pretty enough.
I have been struggling with trying to remove OnGlobalLayoutListener once it gets triggered but that throws an IllegalStateException. Since what I need is to scroll my recyclerView to the second element what I did was to check if it already have children and if it is the first time this is true, only then I do the scroll:
public class MyActivity extends BaseActivity implements BalanceView {
...
private boolean firstTime = true;
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
ViewTreeObserver vto = myRecyclerView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (myRecyclerView.getChildCount() > 0 && MyActivity.this.firstTime){
MyActivity.this.firstTime = false;
scrollToSecondPosition();
}
}
});
}
...
private void scrollToSecondPosition() {
// do the scroll
}
}
HTH someone!
(Of course, this was inspired on #andrino and #Phatee answers)
Here is an alternative way:
You can load your recycler view in a thread. Like this
First, create a TimerTask
void threadAliveChecker(final Thread thread){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
if(!thread.isAlive()){
runOnUiThread(new Runnable() {
#Override
public void run() {
// stop your progressbar here
}
});
}
}
},500,500);
}
Second, create a runnable
Runnable myRunnable = new Runnable() {
#Override
public void run() {
runOnUiThread(new Runnable() {
#Override
public void run() {
// load recycler view from here
// you can also toast here
}
});
}
};
Third, create a thread
Thread myThread = new Thread(myRunnable);
threadAliveChecker();
// start showing progress bar according to your need (call a method)
myThread.start();
Understanding the above code now:
TimerTask - It will run and will check the thread
(every 500 milliseconds) is running or completed.
Runnable - runnable is just like a method, here you have written the code that is needed to be done in that thread. So our recycler view will be called from this runnable.
Thread - Runnable will be called using this thread. So we have started this thread and when the recyclerView load (runnable code load) then this thread will be completed (will not live in programming words).
So our timer is checking the thread is alive or not and when the thread.isAlive is false then we will remove the progress Bar.
If you are using the android-ktx library and if you need to perform an action after positioning all elements of the Activity, you can use this method:
// define 'afterMeasured' Activity listener:
fun Activity.afterMeasured(f: () -> Unit) {
window.decorView.findViewById<View>(android.R.id.content).doOnNextLayout {
f()
}
}
// in Activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(...)
afterMeasured {
// do something here
}
}
This is how I did it
recyclerView.setLayoutManager(new LinearLayoutManager(this){
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
//code to run after loading recyclerview
new GuideView.Builder(MainActivity.this)
.setTargetView(targetView)
.setGravity(Gravity.auto)
.setDismissType(DismissType.outside)
.setContentTextSize(18)
.build()
.show();
}
});
I wish this will help you.
You can use with this approach
if ((adapterPosition + 1) == mHistoryResponse.size) {
Log.d("debug", "process done")
}
get the adapterPosition with plus 1 and check it with your data classes size, if it has same size, the process is practically complete.
For those that are not using Kotlin and are still struggling, I took a fast look at the doOnNextLayout(crossinline action: (view: T) -> Unit) solution they implemented, and it is pretty simple.
IF you are NOT working with a custom RecyclerView (CustomRecyclerView extends RecyclerView), you may want to rethink it as this will bring a lot of benefits you may want to add in the future (smooth scroll to position, vertical dividers, etc..)
Inside the CustomRecyclerView.class
public void doOnNextLayout(Consumer<List<View>> onChange) {
addOnLayoutChangeListener(
new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
onChange.accept(getChildren());
removeOnLayoutChangeListener(this);
}
}
);
}
The getChildren() method is building a List of size getChildCount(); and a add(getChild(i)) on each iteration.
Now...
One important aspect about the code is this: removeOnLayoutChangeListener(this);
This means that the devs are asking for you to execute this before each list submission to the adapter.
In theory we could only place the listener ONCE upon RecyclerView creation (which IMO would be cheaper/better) + because we are retrieving the views, we could retrieve their respective binds with DataBindingUtils. and get whatever data the adapter gave the view onBind via their DataBind.
To do this tho it requires more code.
First the adapter needs to be aware of the Fragment they inhabit, OR the RecyclerView::setAdapter needs to provide a ViewLifeCyclerOwner, a third easier option is to provide the adapter with a onViewDestroy() method, and execute it on Fragment's onDestroyView() method.
#Override
public void onDestroyView() {
super.onDestroyView();
adater.onViewDestroyed();
}
by overriding the onAttachedToRecyclerView, we are able to attach them as observers.
private final List<Runnable> submitter = new ArrayList<>();
#Override
public void onAttachedToRecyclerView(#NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (recyclerView instanceof CustomRecyclerView) {
submitter.add(((CustomRecyclerView) recyclerView)::onSubmit);
}
}
Where the onSubmit method on the CustomRecyclerView side will provide a boolean that will tell the recyclerView whether a list is being submitted.
private boolean submitting;
public void doOnNextLayout(Consumer<List<View>> onChange) {
addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (submitting) {
onChange.accept(getChildren());
submitting = false;
}
}
);
}
public void onSubmit() {
submitting = true;
}
Each Runnable will be executed at the moment of list submission:
In the case of the ListAdapter there are 2 possible entry points:
private void notifyRVs() {
for (Runnable r:submitter
) {
r.run();
}
}
#Override
public void submitList(#Nullable List<X> list, #Nullable Runnable commitCallback) {
notifyRVs();
super.submitList(list, commitCallback);
}
#Override
public void submitList(#Nullable List<X> list) {
notifyRVs();
super.submitList(list);
}
Now to prevent memory leaks we must clear the List of Runnables on ViewDestroyed()
inside the Adapter...
public void onViewDestroyed() {
submitter.clear();
}
Now because the functionality of the method changed we should rename it, and decouple the Consumer<List> from the LayoutChangeListener()
private Consumer<List<View>> onChange = views -> {};
public void setOnListSubmitted(Consumer<List<View>> onChange) {
this.onChange = onChange;
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
//Read attributes
setOnListSubmissionListener();
}
private void setOnListSubmissionListener() {
addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (submitting) {
onChange.accept(getChildren());
submitting = false;
}
}
);
}
What worked for me was to add the listener after setting the RecyclerView adapter.
ServerRequest serverRequest = new ServerRequest(this);
serverRequest.fetchAnswersInBackground(question_id, new AnswersDataCallBack()
{
#Override
public void done(ArrayList<AnswerObject> returnedListOfAnswers)
{
mAdapter = new ForumAnswerRecyclerViewAdapter(returnedListOfAnswers, ForumAnswerActivity.this);
recyclerView.setAdapter(mAdapter);
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
progressDialog.dismiss();
}
});
}
});
This dismisses the "progressDialog" after the global layout state or the visibility of views within the view tree changes.
// Another way
// Get the values
Maybe<List<itemClass>> getItemClass(){
return /* */
}
// Create a listener
void getAll(DisposableMaybeObserver<List<itemClass>> dmo) {
getItemClass().subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dmo);
}
// In the code where you want to track the end of loading in recyclerView:
DisposableMaybeObserver<List<itemClass>> mSubscriber = new DisposableMaybeObserver<List<itemClass>>() {
#Override
public void onSuccess(List<itemClass> item_list) {
adapter.setWords(item_list);
adapter.notifyDataSetChanged();
Log.d("RECYCLER", "DONE");
}
#Override
public void onError(Throwable e) {
Log.d("RECYCLER", "ERROR " + e.getMessage());
}
#Override
public void onComplete() {
Log.d("RECYCLER", "COMPLETE");
}
};
void getAll(mSubscriber);
//and
#Override
public void onDestroy() {
super.onDestroy();
mSubscriber.dispose();
Log.d("RECYCLER","onDestroy");
}
recyclerView.getChildAt(recyclerView.getChildCount() - 1).postDelayed(new Runnable() {
#Override
public void run() {
//do something
}
}, 300);
RecyclerView only lays down specific number of items at a time, we can get the number by calling getChildCount(). Next, we need to get the last item by calling getChildAt (int index). The index is getChildCount() - 1.
I'm inspired by this person answer and I can't find his post again. He said it's important to use postDelayed() instead of regular post() if you want to do something to the last item. I think it's to avoid NullPointerException. 300 is delayed time in ms. You can change it to 50 like that person did.
I'm almost sold to RxJava, which is a perfect companion to Retrofit, but I'm struggling into a common pattern while migrating my code: to save bandwidth, I'd like to lazily fetch (paginated) objects from my webservice as needed, while my listview (or recyclerview) is scrolling using reactive programming.
My previous code was doing the job perfectly, but reactive programming seems worth the try.
Listening to listview/recyclerview scrolling (and other boring stuffs) isn't the concern and getting an Observable is easy using Retrofit:
#GET("/api/messages")
Observable<List<Message>> getMessages(#Path("offset") int offset, #Path("limit") int limit);
I just can't figure out the pattern to use in reactive programming.
The Concat operator seems a good starting point, along with ConnectableObservable at some point to defer emission and maybe flatMap, but how ?
EDIT:
Here's my current (naive) solution:
public interface Paged<T> {
boolean isLoading();
void cancel();
void next(int count);
void next(int count, Scheduler scheduler);
Observable<List<T>> asObservable();
boolean hasCompleted();
int position();
}
And my implementation using a subject:
public abstract class SimplePaged<T> implements Paged<T> {
final PublishSubject<List<T>> subject = PublishSubject.create();
private volatile boolean loading;
private volatile int offset;
private Subscription subscription;
#Override
public boolean isLoading() {
return loading;
}
#Override
public synchronized void cancel() {
if(subscription != null && !subscription.isUnsubscribed())
subscription.unsubscribe();
if(!hasCompleted())
subject.onCompleted();
subscription = null;
loading = false;
}
#Override
public void next(int count) {
next(count, null);
}
#Override
public synchronized void next(int count, Scheduler scheduler) {
if (isLoading())
throw new IllegalStateException("you can't call next() before onNext()");
if(hasCompleted())
throw new IllegalStateException("you can't call next() after onCompleted()");
loading = true;
Observable<List<T>> obs = onNextPage(offset, count).single();
if(scheduler != null)
obs = obs.subscribeOn(scheduler); // BEWARE! onNext/onError/onComplete will happens on that scheduler!
subscription = obs.subscribe(this::onNext, this::onError, this::onComplete);
}
#Override
public Observable<List<T>> asObservable() {
return subject.asObservable();
}
#Override
public boolean hasCompleted() {
return subject.hasCompleted();
}
#Override
public int position() {
return offset;
}
/* Warning: functions below may be called from another thread */
protected synchronized void onNext(List<T> items) {
if (items != null)
offset += items.size();
loading = false;
if (items == null || items.size() == 0)
subject.onCompleted();
else
subject.onNext(items);
}
protected synchronized void onError(Throwable t) {
loading = false;
subject.onError(t);
}
protected synchronized void onComplete() {
loading = false;
}
abstract protected Observable<List<T>> onNextPage(int offset, int count);
}
Here's one out of a few potential ways to handle reactive paging. Let's assume we have a method getNextPageTrigger which returns an Observable emits some event object when the scroll listener (or whatever input) wants a new page to be loaded. In real life it would probably have the debounce operator, but in addition to that we'll make sure we only trigger it after the latest page has loaded.
We also define a method to unwrap the messages from their list:
Observable<Message> getPage(final int page) {
return service.getMessages(page * PAGE_SIZE, PAGE_SIZE)
.flatMap(messageList -> Observable.from(messageList));
}
Then we can make the actual fetching logic:
// Start with the first page.
getPage(0)
// Add on each incremental future page.
.concatWith(Observable.range(1, Integer.MAX_VALUE)
// Uses a little trick to get the next page to wait for a signal to load.
// By ignoring all actual elements emitted and casting, the trigger must
// complete before the actual page request will be made.
.concatMap(page -> getNextPageTrigger().limit(1)
.ignoreElements()
.cast(Message.class)
.concatWith(getPage(page))) // Then subscribe, etc..
This is still missing a couple potentially important things:
1 - This obviously doesn't know when to stop fetching additional pages, which means once it hits the end, depending on what the server returns, it could either keep hitting errors or empty results every time scroll is triggered. Approaches to solving this depend on how you signal to the client that there are no more pages to load.
2 - If you need error retries, I would suggest looking into the retryWhen operator. Otherwise, common network errors could cause an error in a page load to propagate.