Trying to get all the channels from Twilio chat using the twilio SDK. Want to wait for the channel list to load(using Observables) and then display it in my UI. Below is a rough idea of what i'm trying to do:
private List<Paginator<ChannelDescriptor> getAllChannels() {
ChatClient.Properties props = new ChatClient.Properties.Builder()
.createProperties();
ChatClient chatClient = ChatClient.create(context.getApplicationContext(),
accessToken,
props,
null);
List<Paginator<ChannelDescriptor> channelList = new ArrayList<>()
chatClient.getChannels().getUserChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> firstPaginator) {
channelList.add(firstPaginator);
Paginator<ChannelDescriptor> nextPaginator = firstPaginator;
while (nextPaginator != null && nextPaginator.hasNextPage()) {
nextPaginator = loadNextChatChannelPage(firstPaginator);
if(nextPaginator != null) {
channelList.add(nextPaginator);
}
}
}
});
return channelList;
}
public Paginator<ChannelDescriptor> loadNextChatChannelPage(Paginator<ChannelDescriptor> paginator) {
paginator.requestNextPage(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
return channelDescriptorPaginator;
}
#Override
public void onError(ErrorInfo errorInfo) {
return null.
}
}));
}
What i ended up doing is this:
/**
* Start loading the Twilio chat channel pages
*
* #return Single containing a list of all the chat channel pages
*/
public Single<List<Paginator<ChannelDescriptor>>> loadAllChatChannelPages(ChatClientManager chatClientManager) {
return Single.create(emitter -> chatClientManager.getChatClient().getChannels().getUserChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
if(channelDescriptorPaginator != null) {
emitter.onSuccess(channelDescriptorPaginator);
}
}
#Override
public void onError(ErrorInfo errorInfo) {
String errorMessage = "";
if(errorInfo != null) {
errorMessage = errorInfo.getMessage();
}
emitter.onError(new Throwable(errorMessage));
}
})).subscribeOn(Schedulers.io())
.flatMap(firstPaginator -> {
if(firstPaginator != null) {
return loadChannelPaginator((Paginator<ChannelDescriptor>) firstPaginator).toList()
.subscribeOn(Schedulers.io());
} else {
return Single.error(new Throwable("Could not get chat channels"));
}
});
}
/**
* Recursively loads the chat channel pages and returns them as a single observable
*
* #param paginator this needs to be the first chat channel paginator from the chat client
* #return Observable containing a flattened version of all the available chat channel paginators
*/
private Observable<Paginator<ChannelDescriptor>> loadChannelPaginator(Paginator<ChannelDescriptor> paginator) {
if (paginator.hasNextPage()) {
return Observable.mergeDelayError(
Observable.just(paginator),
loadNextChatChannelPage(paginator)
.flatMap(this::loadChannelPaginator));
}
return Observable.just(paginator);
}
/**
* Loads a single chat channel page
*
* #param previousPage the previous page of chat channels
* #return Observable containing the next chat channel page
*/
private Observable<Paginator<ChannelDescriptor>> loadNextChatChannelPage(Paginator<ChannelDescriptor> previousPage) {
return Observable.create(emitter -> previousPage.requestNextPage(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
if(channelDescriptorPaginator != null) {
emitter.onNext(channelDescriptorPaginator);
}
emitter.onComplete();
}
#Override
public void onError(ErrorInfo errorInfo) {
if(errorInfo != null) {
String errorMessage = errorInfo.getMessage();
Timber.e(errorMessage);
}
// emitter.onError(new Throwable(errorMessage));
emitter.onComplete();
}
}));
}
In the above code loadAllChatChannelPages loads the first paginator.
If that's not null then loadChannelPaginator takes over and recursively grabs each next paginator by executing loadNextChatChannelPage, a method that returns an observable for each single paginator.
Then mergeDelayError flattens all the paginators and returns them as one single Observable.
Finally in getAllChannels i apply Observable.toList(), this return a Single containing the list of Paginated chat channels that i needed.
Related
I am using RxJava2.
i have some observable, and few subscribers that can be subscribed for it.
each time when new subscribers arrive, some job should be done and each of subscribers should be notified.
for this i decide to use PublishSubject. but when doOnSubscribe received from firs subscriber, myPublishSubject.hasObservers() return false...
any idea why it happens and how can i fix this?
private val myPublishSubject = PublishSubject.create<Boolean>()
fun getPublishObservable():Observable<Boolean> {
return myPublishSubject.doOnSubscribe {
//do some job when new subscriber arrived and notify all subscribers
//via
myPublishSubject.onNext(true)
}
}
Do I understand it correct, that when doOnSubscribe called it mean that there is at least one subscribers already present?
i did not find ready answer, so i create my own version of subject and call it RefreshSubject.
it based on PublishSubject but with one difference: if you would like to return observable and be notified when new subscriber arrives and ready to receive some data you should use method getSubscriberReady.
here a small example:
private RefreshSubject<Boolean> refreshSubject = RefreshSubject.create();
//ordinary publish behavior
public Observable<Boolean> getObservable(){
return refreshSubject;
}
//refreshSubject behaviour
public Observable<Boolean> getRefreshObserver(){
return refreshSubject.getSubscriberReady(new Action() {
#Override
public void run() throws Exception {
//new subscriber arrives and ready to receive some data
//so you can make some data request and all your subscribers (with new one just arrived)
//will receive new content
}
});
}
and here is full class:
public class RefreshSubject<T> extends Subject<T> {
/** The terminated indicator for the subscribers array. */
#SuppressWarnings("rawtypes")
private static final RefreshSubject.RefreshDisposable[] TERMINATED = new RefreshSubject.RefreshDisposable[0];
/** An empty subscribers array to avoid allocating it all the time. */
#SuppressWarnings("rawtypes")
private static final RefreshSubject.RefreshDisposable[] EMPTY = new RefreshSubject.RefreshDisposable[0];
/** The array of currently subscribed subscribers. */
private final AtomicReference<RefreshDisposable<T>[]> subscribers;
/** The error, write before terminating and read after checking subscribers. */
Throwable error;
/**
* Constructs a RefreshSubject.
* #param <T> the value type
* #return the new RefreshSubject
*/
#CheckReturnValue
public static <T> RefreshSubject<T> create() {
return new RefreshSubject<T>();
}
/**
* Constructs a RefreshSubject.
* #since 2.0
*/
#SuppressWarnings("unchecked")
private RefreshSubject() {
subscribers = new AtomicReference<RefreshSubject.RefreshDisposable<T>[]>(EMPTY);
}
#Override
public void subscribeActual(Observer<? super T> t) {
RefreshSubject.RefreshDisposable<T> ps = new RefreshSubject.RefreshDisposable<T>(t, RefreshSubject.this);
t.onSubscribe(ps);
if (add(ps)) {
// if cancellation happened while a successful add, the remove() didn't work
// so we need to do it again
if (ps.isDisposed()) {
remove(ps);
}
} else {
Throwable ex = error;
if (ex != null) {
t.onError(ex);
} else {
t.onComplete();
}
}
}
public Observable<T> getSubscriberReady(final Action onReady){
return Observable.create(new ObservableOnSubscribe<T>() {
#Override
public void subscribe(ObservableEmitter<T> e) throws Exception {
add(new RefreshDisposable(e, RefreshSubject.this));
onReady.run();
}
});
}
/**
* Tries to add the given subscriber to the subscribers array atomically
* or returns false if the subject has terminated.
* #param ps the subscriber to add
* #return true if successful, false if the subject has terminated
*/
private boolean add(RefreshSubject.RefreshDisposable<T> ps) {
for (;;) {
RefreshSubject.RefreshDisposable<T>[] a = subscribers.get();
if (a == TERMINATED) {
return false;
}
int n = a.length;
#SuppressWarnings("unchecked")
RefreshSubject.RefreshDisposable<T>[] b = new RefreshSubject.RefreshDisposable[n + 1];
System.arraycopy(a, 0, b, 0, n);
b[n] = ps;
if (subscribers.compareAndSet(a, b)) {
return true;
}
}
}
/**
* Atomically removes the given subscriber if it is subscribed to the subject.
* #param ps the subject to remove
*/
#SuppressWarnings("unchecked")
private void remove(RefreshSubject.RefreshDisposable<T> ps) {
for (;;) {
RefreshSubject.RefreshDisposable<T>[] a = subscribers.get();
if (a == TERMINATED || a == EMPTY) {
return;
}
int n = a.length;
int j = -1;
for (int i = 0; i < n; i++) {
if (a[i] == ps) {
j = i;
break;
}
}
if (j < 0) {
return;
}
RefreshSubject.RefreshDisposable<T>[] b;
if (n == 1) {
b = EMPTY;
} else {
b = new RefreshSubject.RefreshDisposable[n - 1];
System.arraycopy(a, 0, b, 0, j);
System.arraycopy(a, j + 1, b, j, n - j - 1);
}
if (subscribers.compareAndSet(a, b)) {
return;
}
}
}
#Override
public void onSubscribe(Disposable s) {
if (subscribers.get() == TERMINATED) {
s.dispose();
}
}
#Override
public void onNext(T t) {
if (subscribers.get() == TERMINATED) {
return;
}
if (t == null) {
onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
return;
}
for (RefreshSubject.RefreshDisposable<T> s : subscribers.get()) {
s.onNext(t);
}
}
#SuppressWarnings("unchecked")
#Override
public void onError(Throwable t) {
if (subscribers.get() == TERMINATED) {
RxJavaPlugins.onError(t);
return;
}
if (t == null) {
t = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources.");
}
error = t;
for (RefreshSubject.RefreshDisposable<T> s : subscribers.getAndSet(TERMINATED)) {
s.onError(t);
}
}
#SuppressWarnings("unchecked")
#Override
public void onComplete() {
if (subscribers.get() == TERMINATED) {
return;
}
for (RefreshSubject.RefreshDisposable<T> s : subscribers.getAndSet(TERMINATED)) {
s.onComplete();
}
}
#Override
public boolean hasObservers() {
return subscribers.get().length != 0;
}
#Override
public Throwable getThrowable() {
if (subscribers.get() == TERMINATED) {
return error;
}
return null;
}
#Override
public boolean hasThrowable() {
return subscribers.get() == TERMINATED && error != null;
}
#Override
public boolean hasComplete() {
return subscribers.get() == TERMINATED && error == null;
}
/**
* Wraps the actualEmitter subscriber, tracks its requests and makes cancellation
* to remove itself from the current subscribers array.
*
* #param <T> the value type
*/
private static final class RefreshDisposable<T> extends AtomicBoolean implements Disposable {
private static final long serialVersionUID = 3562861878281475070L;
/** The actualEmitter subscriber. */
final Emitter<? super T> actualEmitter;
/** The actualEmitter subscriber. */
final Observer<? super T> actualObserver;
/** The subject state. */
final RefreshSubject<T> parent;
/**
* Constructs a PublishSubscriber, wraps the actualEmitter subscriber and the state.
* #param actualEmitter the actualEmitter subscriber
* #param parent the parent RefreshProcessor
*/
RefreshDisposable(Emitter<? super T> actualEmitter, RefreshSubject<T> parent) {
this.actualEmitter = actualEmitter;
this.parent = parent;
actualObserver = null;
}
/**
* Constructs a PublishSubscriber, wraps the actualEmitter subscriber and the state.
* #param actualObserver the actualObserver subscriber
* #param parent the parent RefreshProcessor
*/
RefreshDisposable(Observer<? super T> actualObserver, RefreshSubject<T> parent) {
this.actualObserver = actualObserver;
this.parent = parent;
actualEmitter = null;
}
public void onNext(T t) {
if (!get()) {
if (actualEmitter != null)
actualEmitter.onNext(t);
if (actualObserver != null)
actualObserver.onNext(t);
}
}
public void onError(Throwable t) {
if (get()) {
RxJavaPlugins.onError(t);
} else {
if (actualEmitter != null)
actualEmitter.onError(t);
if (actualObserver != null)
actualObserver.onError(t);
}
}
public void onComplete() {
if (!get()) {
if (actualEmitter != null)
actualEmitter.onComplete();
if (actualObserver != null)
actualObserver.onComplete();
}
}
#Override
public void dispose() {
if (compareAndSet(false, true)) {
parent.remove(this);
}
}
#Override
public boolean isDisposed() {
return get();
}
}
}
I am currently trying to implement the new ViewModels in the architecture components with an API request from retrofit and Okhttp, everything is working but I can't figure out how to pass an error response from retrofit to LiveDataReactiveStreams.fromPublisher and then upstream to the observer in the fragment. This is what I have so far:
public class ShowListViewModel extends AndroidViewModel {
private final ClientAdapter clientAdapter;
private LiveData<List<Show>> shows;
public ShowListViewModel(Application application) {
super(application);
clientAdapter = new ClientAdapter(getApplication().getApplicationContext());
loadShows();
}
public LiveData<List<Show>> getShows() {
if (shows == null) {
shows = new MutableLiveData<>();
}
return shows;
}
void loadShows() {
shows = LiveDataReactiveStreams.fromPublisher(Observable.fromIterable(ShowsUtil.loadsIds())
.subscribeOn(Schedulers.io())
.flatMap(clientAdapter::getShowWithNextEpisode)
.observeOn(Schedulers.computation())
.toSortedList(new ShowsUtil.ShowComparator())
.observeOn(AndroidSchedulers.mainThread())
.toFlowable());
}
And in the fragment I setup the viewModel with the following in OnCreate:
ShowListViewModel model = ViewModelProviders.of(this).get(ShowListViewModel.class);
model.getShows().observe(this, shows -> {
if (shows == null || shows.isEmpty()) {
//This is where we may have empty list etc....
} else {
//process results from shows list here
}
});
Everything works as expected but currently if we are offline then retrofit is throwing a runtimeException and crashing. I think the problem lies here:
LiveDataReactiveStreams.fromPublisher(Observable.fromIterable(ShowsUtil.loadsIds())
.subscribeOn(Schedulers.io())
.flatMap(clientAdapter::getShowWithNextEpisode)
.observeOn(Schedulers.computation())
.toSortedList(new ShowsUtil.ShowComparator())
.observeOn(AndroidSchedulers.mainThread())
.toFlowable());
}
Normally we would use rxjava2 subscribe and catch the error from retrofit there, but when using LiveDataReactiveStreams.fromPublisher it subscribes to the flowable for us. So how do we pass this error into here:
model.getShows().observe(this, shows -> { //process error in fragment});
Rather than exposing just the list of shows through your LiveData object you would need to wrap the shows and error into a class that can hold the error.
With your example you could do something like this:
LiveDataReactiveStreams.fromPublisher(Observable.fromIterable(ShowsUtil.loadsIds())
.subscribeOn(Schedulers.io())
.flatMap(clientAdapter::getShowWithNextEpisode)
.observeOn(Schedulers.computation())
.toSortedList(new ShowsUtil.ShowComparator())
.observeOn(AndroidSchedulers.mainThread())
.map(Result::success)
.onErrorReturn(Result::error)
.toFlowable());
Where Result is the wrapper class that holds either the error or result
final class Result<T> {
private final T result;
private final Throwable error;
private Result(#Nullable T result, #Nullable Throwable error) {
this.result = result;
this.error = error;
}
#NonNull
public static <T> Result<T> success(#NonNull T result) {
return new Result(result, null);
}
#NonNull
public static <T> Result<T> error(#NonNull Throwable error) {
return new Result(null, error);
}
#Nullable
public T getResult() {
return result;
}
#Nullable
public Throwable getError() {
return error;
}
}
In my case I copied LiveDataReactiveStreams.java into my 'util' package as 'MyLiveDataReactiveStreams.java'. The modified version replaces the RuntimeException with a GreenRobot EventBus post. Then in my app I can subscribe to that event and handle the error appropriately. For this solution I had to add '#SuppressLint("RestrictedApi")' in 3 places. I am not sure if Google allows that for play store apps. This repo contains a complete example: https://github.com/LeeHounshell/Dogs
Below is the relevant code:
// add GreenRobot to your app/build.gradle
def greenrobot_version = '3.2.0'
def timberkt_version = '1.5.1'
implementation "org.greenrobot:eventbus:$greenrobot_version"
implementation "com.github.ajalt:timberkt:$timberkt_version"
//--------------------------------------------------
// next put this in your Activity or Fragment to handle the error
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this);
}
#Subscribe(threadMode = ThreadMode.MAIN)
fun onRxErrorEvent(rxError_event: RxErrorEvent) {
// do your custom error handling here
Toast.makeText(activity, rxError_event.errorDescription, Toast.LENGTH_LONG).show()
}
//--------------------------------------------------
// create a new class in 'util' to post the error
import com.github.ajalt.timberkt.Timber
import org.greenrobot.eventbus.EventBus
class RxErrorEvent(val description: String) {
private val _tag = "LEE: <" + RxErrorEvent::class.java.simpleName + ">"
lateinit var errorDescription: String
init {
errorDescription = description
}
fun post() {
Timber.tag(_tag).e("post $errorDescription")
EventBus.getDefault().post(this)
}
}
//--------------------------------------------------
// and use MyLiveDataReactiveStreams (below) instead of LiveDataReactiveStreams
/*
* This is a modified version of androidx.lifecycle.LiveDataReactiveStreams
* The original LiveDataReactiveStreams object can't handle RxJava error conditions.
* Now errors are emitted as RxErrorEvent objects using the GreenRobot EventBus.
*/
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import com.github.ajalt.timberkt.Timber;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import java.util.concurrent.atomic.AtomicReference;
/**
* Adapts {#link LiveData} input and output to the ReactiveStreams spec.
*/
#SuppressWarnings("WeakerAccess")
public final class MyLiveDataReactiveStreams {
private final static String _tag = "LEE: <" + MyLiveDataReactiveStreams.class.getSimpleName() + ">";
private MyLiveDataReactiveStreams() {
}
/**
* Adapts the given {#link LiveData} stream to a ReactiveStreams {#link Publisher}.
*
* <p>
* By using a good publisher implementation such as RxJava 2.x Flowables, most consumers will
* be able to let the library deal with backpressure using operators and not need to worry about
* ever manually calling {#link Subscription#request}.
*
* <p>
* On subscription to the publisher, the observer will attach to the given {#link LiveData}.
* Once {#link Subscription#request} is called on the subscription object, an observer will be
* connected to the data stream. Calling request(Long.MAX_VALUE) is equivalent to creating an
* unbounded stream with no backpressure. If request with a finite count reaches 0, the observer
* will buffer the latest item and emit it to the subscriber when data is again requested. Any
* other items emitted during the time there was no backpressure requested will be dropped.
*/
#NonNull
public static <T> Publisher<T> toPublisher(
#NonNull LifecycleOwner lifecycle, #NonNull LiveData<T> liveData) {
return new MyLiveDataReactiveStreams.LiveDataPublisher<>(lifecycle, liveData);
}
private static final class LiveDataPublisher<T> implements Publisher<T> {
final LifecycleOwner mLifecycle;
final LiveData<T> mLiveData;
LiveDataPublisher(LifecycleOwner lifecycle, LiveData<T> liveData) {
this.mLifecycle = lifecycle;
this.mLiveData = liveData;
}
#Override
public void subscribe(Subscriber<? super T> subscriber) {
subscriber.onSubscribe(new MyLiveDataReactiveStreams.LiveDataPublisher.LiveDataSubscription<T>(subscriber, mLifecycle, mLiveData));
}
static final class LiveDataSubscription<T> implements Subscription, Observer<T> {
final Subscriber<? super T> mSubscriber;
final LifecycleOwner mLifecycle;
final LiveData<T> mLiveData;
volatile boolean mCanceled;
// used on main thread only
boolean mObserving;
long mRequested;
// used on main thread only
#Nullable
T mLatest;
LiveDataSubscription(final Subscriber<? super T> subscriber,
final LifecycleOwner lifecycle, final LiveData<T> liveData) {
this.mSubscriber = subscriber;
this.mLifecycle = lifecycle;
this.mLiveData = liveData;
}
#Override
public void onChanged(#Nullable T t) {
if (mCanceled) {
return;
}
if (mRequested > 0) {
mLatest = null;
mSubscriber.onNext(t);
if (mRequested != Long.MAX_VALUE) {
mRequested--;
}
} else {
mLatest = t;
}
}
#SuppressLint("RestrictedApi")
#Override
public void request(final long n) {
if (mCanceled) {
return;
}
ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
#Override
public void run() {
if (mCanceled) {
return;
}
if (n <= 0L) {
mCanceled = true;
if (mObserving) {
mLiveData.removeObserver(MyLiveDataReactiveStreams.LiveDataPublisher.LiveDataSubscription.this);
mObserving = false;
}
mLatest = null;
mSubscriber.onError(
new IllegalArgumentException("Non-positive request"));
return;
}
// Prevent overflowage.
mRequested = mRequested + n >= mRequested
? mRequested + n : Long.MAX_VALUE;
if (!mObserving) {
mObserving = true;
mLiveData.observe(mLifecycle, MyLiveDataReactiveStreams.LiveDataPublisher.LiveDataSubscription.this);
} else if (mLatest != null) {
onChanged(mLatest);
mLatest = null;
}
}
});
}
#SuppressLint("RestrictedApi")
#Override
public void cancel() {
if (mCanceled) {
return;
}
mCanceled = true;
ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
#Override
public void run() {
if (mObserving) {
mLiveData.removeObserver(MyLiveDataReactiveStreams.LiveDataPublisher.LiveDataSubscription.this);
mObserving = false;
}
mLatest = null;
}
});
}
}
}
/**
* Creates an observable {#link LiveData} stream from a ReactiveStreams {#link Publisher}}.
*
* <p>
* When the LiveData becomes active, it subscribes to the emissions from the Publisher.
*
* <p>
* When the LiveData becomes inactive, the subscription is cleared.
* LiveData holds the last value emitted by the Publisher when the LiveData was active.
* <p>
* Therefore, in the case of a hot RxJava Observable, when a new LiveData {#link Observer} is
* added, it will automatically notify with the last value held in LiveData,
* which might not be the last value emitted by the Publisher.
* <p>
* Note that LiveData does NOT handle errors and it expects that errors are treated as states
* in the data that's held. In case of an error being emitted by the publisher, an error will
* be propagated to the main thread and the app will crash.
*
* #param <T> The type of data hold by this instance.
*/
#NonNull
public static <T> LiveData<T> fromPublisher(#NonNull Publisher<T> publisher) {
return new MyLiveDataReactiveStreams.PublisherLiveData<>(publisher);
}
/**
* Defines a {#link LiveData} object that wraps a {#link Publisher}.
*
* <p>
* When the LiveData becomes active, it subscribes to the emissions from the Publisher.
*
* <p>
* When the LiveData becomes inactive, the subscription is cleared.
* LiveData holds the last value emitted by the Publisher when the LiveData was active.
* <p>
* Therefore, in the case of a hot RxJava Observable, when a new LiveData {#link Observer} is
* added, it will automatically notify with the last value held in LiveData,
* which might not be the last value emitted by the Publisher.
*
* <p>
* Note that LiveData does NOT handle errors and it expects that errors are treated as states
* in the data that's held. In case of an error being emitted by the publisher, an error will
* be propagated to the main thread and the app will crash.
*
* #param <T> The type of data hold by this instance.
*/
private static class PublisherLiveData<T> extends LiveData<T> {
private final Publisher<T> mPublisher;
final AtomicReference<MyLiveDataReactiveStreams.PublisherLiveData.LiveDataSubscriber> mSubscriber;
PublisherLiveData(#NonNull Publisher<T> publisher) {
mPublisher = publisher;
mSubscriber = new AtomicReference<>();
}
#Override
protected void onActive() {
super.onActive();
MyLiveDataReactiveStreams.PublisherLiveData.LiveDataSubscriber s = new MyLiveDataReactiveStreams.PublisherLiveData.LiveDataSubscriber();
mSubscriber.set(s);
mPublisher.subscribe(s);
}
#Override
protected void onInactive() {
super.onInactive();
MyLiveDataReactiveStreams.PublisherLiveData.LiveDataSubscriber s = mSubscriber.getAndSet(null);
if (s != null) {
s.cancelSubscription();
}
}
final class LiveDataSubscriber extends AtomicReference<Subscription>
implements Subscriber<T> {
#Override
public void onSubscribe(Subscription s) {
if (compareAndSet(null, s)) {
s.request(Long.MAX_VALUE);
} else {
s.cancel();
}
}
#Override
public void onNext(T item) {
postValue(item);
}
#SuppressLint("RestrictedApi")
#Override
public void onError(final Throwable ex) {
mSubscriber.compareAndSet(this, null);
ArchTaskExecutor.getInstance().executeOnMainThread(new Runnable() {
#Override
public void run() {
//NOTE: Errors are be handled upstream
Timber.tag(_tag).e("LiveData does not handle errors. Errors from publishers are handled upstream via EventBus. error: " + ex);
RxErrorEvent rx_event = new RxErrorEvent(ex.toString());
rx_event.post();
}
});
}
#Override
public void onComplete() {
mSubscriber.compareAndSet(this, null);
}
public void cancelSubscription() {
Subscription s = get();
if (s != null) {
s.cancel();
}
}
}
}
}
I have used a solution with LiveDataReactiveStreams.fromPublisher along with a generic wrapper class Result that was based on the following class.
https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt
The idea is similar to https://stackoverflow.com/a/51669953/4266287
This pattern can be used regardless of using LiveData and the main idea is to be able to handle errors without ending the stream. So it works similar to RxRelay where even your errors will be emitted using onNext and the actual meaning comes from the data, whether the result or error are present.
If this will be part of the backbone of your project, you can create some extra framework to make all this transparent. For example, you can create your own class that would be something like a Resource<T> aware stream that has an observe method that mimics the way you call RxJava's observe but "unwrap" the data and call the right callback:
fun subscribe(
onNext: (T) -> Unit,
onError: ((Throwable) -> Unit)? = null
) {
val handleOnNext: (Resource<T>) -> Unit = { resource: Resource<T> ->
when (resource.status) {
Status.SUCCESS -> resource.data?.let(onNext)
Status.ERROR -> resource.error?.let { onError?.invoke(it) ?: throw it }
}
}
publishSubject
.subscribeOn(subscribeOn)
.observeOn(observeOn)
.run { subscribe(handleOnNext) }
.addTo(compositeDisposable)
}
I wanted to create a wrapper for api calls in retrofit so I can display ProgressDialog at common place & handle common response.
I achieved this by creating wrapper like this
public static <T> Observable<T> callApiWrapper(final Context context,
final boolean shouldShowProgress,
final String message,
final Observable<T> source) {
final ProgressDialog progressDialog = new ProgressDialog(context);
if (shouldShowProgress) {
if (!TextUtils.isEmpty(message))
progressDialog.setMessage(message);
else
progressDialog.setMessage(context.getString(R.string.please_wait));
}
return source.lift(new Observable.Operator<T, T>() {
#Override
public Subscriber<? super T> call(final Subscriber<? super T> child) {
return new Subscriber<T>() {
#Override
public void onStart() {
super.onStart();
if (shouldShowProgress) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
progressDialog.show();
}
});
}
child.onStart();
}
#Override
public void onCompleted() {
if (shouldShowProgress && progressDialog.isShowing())
progressDialog.dismiss();
child.onCompleted();
}
#Override
public void onError(Throwable e) {
if (shouldShowProgress && progressDialog.isShowing())
progressDialog.dismiss();
child.onError(e);
}
#Override
public void onNext(T t) {
/*
Handle Invalid API response
*/
if (((BaseResponse) t).getStatus() == RestParams.Codes.INVALID_API_KEY) {
mCommonDataModel.setApiKey("");
getCommonApiService().getApiKey()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ResponseBody>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(ResponseBody responseBody) {
try {
String response = responseBody.string();
JSONObject jsonObject = new JSONObject(response);
String key = jsonObject.optString("KEY");
if (!TextUtils.isEmpty(key))
mCommonDataModel.setApiKey(key);
callApiWrapper(context, shouldShowProgress,
message, source)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
if (shouldShowProgress && progressDialog.isShowing())
progressDialog.dismiss();
child.onNext(t);
}
}
};
}
});
}
In the above code, I check that if I get specific status code like Invalid API KEY, then I am calling an API to get the new API key instead of giving the status directly to original subscriber.
Once I get the new API key successfully, I call the wrapper recursively & try to give the response to original subscriber. But the problem is Original Subscriber is not getting onNext callback
What am I missing here? Is there any other way of achieving what I am trying to do?
You need to add some retry logic in case you get an invalid key failure so something like
source.flatMap(
t ->
{
if (((BaseResponse) t).getStatus() == RestParams.Codes.INVALID_API_KEY) {
return Observable.error(new InvalidKeyException("The key is not valid"));
}
else {
return Observable.just(t);
}
}
)
.retryWhen(
errors ->
errors.flatMap(error -> {
if (error instanceof InvalidKeyException()) {
return getCommonApiService().getApiKey()
.flatMap(
responseBody -> {
String response = responseBody.string();
JSONObject jsonObject = new JSONObject(response);
String key = jsonObject.optString("KEY");
if (TextUtils.isEmpty(key))
return Observable.error();
else {
return Observable.just(key);
}})
.doOnNext( key -> mCommonDataModel.setApiKey(key));
}
// For anything else, don't retry
return Observable.error(error);
}))
.subscribe(/* do what you need to do with the results*/)
In order to add the side effects i.e. enable progress bar when you start the subscription and dismiss it when you've finished something like
modifiedSource.doOnSubscribe(/* progress bar show logic */)
.doOnTerminate(/* progress bar dismiss logic */)
Finally I managed to create a wrapper which handles for me the common Progressbar & retry logic in case of Invalid Key API Response. This kind of wrapper might be useful if in many cases. Thanks to #JohnWowUs for his answer which helped me to understand things & implement this wrapper.
Here's the working code
private static final int MAX_RETRIES = 2;
private static int sCurrentRetryAttempt = 0;
/**
* Common Wrapper for calling API.
*
* #param context context for showing progress dialog
* #param shouldShowProgress boolean which indicated if progress dialog should be shown or not
* #param message message to be shown in progress dialog. if null passed, then "Please wait..." will be shown
* #param source original observable
* #return observable to which observer can subscribe
*/
public static <T> Observable<T> callApiWrapper(final Context context,
final boolean shouldShowProgress,
final String message,
final Observable<T> source) {
// Progress Dialog
final ProgressDialog progressDialog = setupProgressDialog(context, shouldShowProgress, message);
if (progressDialog != null) progressDialog.show();
return source
.flatMap(new Func1<T, Observable<T>>() {
#Override
public Observable<T> call(T t) {
/*
* Check if the response contains invalid key status code.
*/
if (t instanceof BaseResponse) {
if (((BaseResponse) t).getStatus() == RestParams.Codes.INVALID_API_KEY) {
return Observable.error(new InvalidKeyException("Invalid key"));
}
}
/*
* We are here, that means, there wasn't invalid key status code.
* So we wouldn't like to handle it so just return to original subscriber
*/
if (progressDialog != null && progressDialog.isShowing())
progressDialog.dismiss();
return Observable.just(t);
}
}).retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
#Override
public Observable<?> call(Observable<? extends Throwable> observable) {
return observable.flatMap(new Func1<Throwable, Observable<?>>() {
#Override
public Observable<?> call(final Throwable throwable) {
if (throwable instanceof InvalidKeyException) {
/*
* Check for retry limit. if we already have retried enough, then
* we should tell the original subscriber about the error as it
* doesn't seems recoverable.
*/
if (sCurrentRetryAttempt >= MAX_RETRIES) {
if (progressDialog != null && progressDialog.isShowing())
progressDialog.dismiss();
//Resetting the attempts to 0
sCurrentRetryAttempt = 0;
return Observable.error(throwable);
}
//Increase the attempt counter
sCurrentRetryAttempt += 1;
return getCommonApiService().getApiKey()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<ResponseBody, Observable<?>>() {
#Override
public Observable<?> call(ResponseBody responseBody) {
try {
/*
* Check if we succeed in our attempt to handle
* invalid key
*/
if (processApiKey(responseBody)) {
/*
* We succeeded in our attempts to handle
* invalid api key, so we will return the
* original subscriber what it wanted.
*/
return callApiWrapper(context,
shouldShowProgress, message, source);
} else
return Observable.just(throwable);
} catch (Exception e) {
/*
* We are here that means something went wrong,
* so we will retry silently.
*/
return Observable.just(throwable);
}
}
});
} else {
/*
* For any other error, we are not going to handle right now,
* so just return
*/
return Observable.error(throwable);
}
}
});
}
});
}
& using this is same as normal like:
RestClient.callApiWrapper(mContext, true, null,
RestClient.getAuthenticationApiService().socialLogIn(name, email, singInVia, mobile, "android", deviceToken))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<BaseResponse<RegistrationResponse>>() {
//...
}
I'm trying to have user registration for my android application. I'm able to successfully make user register and store their details in Cloudant. They can also login using the phone they had used to register.
However, when I try using another phone to login the account, it doesn't work. Is possible to replicate all data from Cloudant so that users can also login to other phones too? Here is my code:
public class CloudantConnect {
private static final String TAG = CloudantConnect.class.getSimpleName();
private static final String DATASTORE_DIRECTORY = "data";
private Datastore datastore;
private IndexManager indexManager;
private Replicator push_replicator;
private Replicator pull_replicator;
private Context context;
private final Handler handler;
private RegisterActivity register_listener;
public CloudantConnect(Context context, String datastore_name) {
this.context = context;
// Set up information within its own folder in the application
File path = this.context.getApplicationContext().getDir(DATASTORE_DIRECTORY, Context.MODE_PRIVATE);
DatastoreManager manager = new DatastoreManager(path.getAbsolutePath());
try {
this.datastore = manager.openDatastore(datastore_name);
} catch (DatastoreNotCreatedException e) {
Log.e(TAG, "Unable to open Datastore", e);
}
// Reach here if datastore successfully created
Log.d(TAG, "Successfully set up database at" + path.getAbsolutePath());
// Set up replicator objects
try {
this.reloadReplicationSettings();
} catch (URISyntaxException e) {
Log.e(TAG, "Unable to construct remote URI from configuration", e);
}
this.handler = new Handler(Looper.getMainLooper());
Log.d(TAG, "CloudantConnect set up " + path.getAbsolutePath());
}
/**
* Creates new document for user details database storage
* #param user to store user details into database
* #return document of user details stored
*/
public User createNewUserDocument(User user) {
MutableDocumentRevision revision = new MutableDocumentRevision();
revision.body = DocumentBodyFactory.create(user.asMap());
try {
BasicDocumentRevision created = this.datastore.createDocumentFromRevision(revision);
return User.fromRevision(created);
} catch (DocumentException e) {
return null;
}
}
/**
* Sets replication listener
*/
public void setReplicationListener(RegisterActivity listener) {
this.register_listener = listener;
}
/**
* Start push replication
*/
public void startPushReplication() {
if(this.push_replicator != null) {
this.push_replicator.start();
} else {
throw new RuntimeException("Push replication not set up correctly");
}
}
/**
* Start pull replication
*/
public void startPullReplication() {
if(this.pull_replicator != null) {
this.pull_replicator.start();
} else {
throw new RuntimeException("Pull replication not set up correctly");
}
}
/**
* Stop running replication
*/
public void stopAllReplication() {
if(this.push_replicator != null) {
this.push_replicator.stop();
}
if(this.pull_replicator != null) {
this.pull_replicator.stop();
}
}
/**
* Stop running replication and reloads replication settings
* from the app's preferences.
*/
public void reloadReplicationSettings() throws URISyntaxException {
this.stopAllReplication();
// Set up new replicator objects
URI uri = this.createServerURI();
// Push replication
PushReplication push = new PushReplication();
push.source = datastore;
push.target = uri;
push_replicator = ReplicatorFactory.oneway(push);
push_replicator.getEventBus().register(this);
// Pull replication
PullReplication pull = new PullReplication();
pull.source = uri;
pull.target = datastore;
pull_replicator = ReplicatorFactory.oneway(pull);
pull_replicator.getEventBus().register(this);
Log.d(TAG, "Set up replicators for URI:" + uri.toString());
}
/**
* Calls when replication is completed
*/
public void complete(ReplicationCompleted rc) {
handler.post(new Runnable() {
#Override
public void run() {
if(register_listener != null) {
register_listener.replicationComplete();
}
}
});
}
/**
* Calls when replication has error
*/
public void error(ReplicationErrored re) {
Log.e(TAG, "Replication error:", re.errorInfo.getException());
handler.post(new Runnable() {
#Override
public void run() {
if(register_listener != null) {
register_listener.replicationError();
}
}
});
}
}
It looks like you've got all the code there to do replication. Do you actually call startPullReplication() from somewhere?
If you want your complete and error callbacks to run when replication completes/fails, you will need to add the #Subscribe annotation on them both so they're triggered when the events are put on the EventBus.
I'm trying to create a custom loader which loads an list of data that works fine but now I want to added endless scrolling in the listview. I thought a logical place would be in the loader since almost all the examples I see on the interwebz have a private field in the custom loader which corresponds with the data to be returned to UI and in the deliverResult there is some code like this
#Override
public void deliverResult(T data) {
T oldData = mData;
mData = data;
if (isStarted()) {
// If the loader is currently started, we can immediately deliver a result
super.deliverResult(mData);
}
}
No I thought that mData still contains the previous list [1,2,3,4,5] cause the loader should cache the data to show it instantaneously on configuration changes. And data is the new list [6,7,8,9,10] for instance. I could just add data to mData, mData.add(data) and we are done. Don't have to repeat the code on multiple places or different adapters. But seemingly this doesn't work, everytime you call restartLoader to load the new data the framework creates a new instance of the Loader. Has anyone else run into this problem before? or should I just do the mData.add(data) in the Adapter or somewhere else in the code.
the full implementation of the custom loader which extends ApiResponseLoader which can also be find below:
public class SearchLoader extends ApiResponseLoader {
private SearchType mSearchType;
private int mOffset;
private String mSearchQuery;
public SearchLoader(Context context, SearchType type, int offset, String query) {
super(context);
mSearchType = type;
mOffset = offset;
mSearchQuery = query;
}
#Override
public ApiResponse loadInBackground() {
try {
Map<String, String> parameters = Utils.parametersMap("q:" + mSearchQuery, "offset:" + String.valueOf(mOffset));
return tryLoadInBackground(parameters);
} catch (Exception e) {
setError(e);
return null;
}
}
public ApiResponse tryLoadInBackground(Map<String, String> parameters) throws Exception {
if (mSearchQuery == null) {
throw new NullPointerException("mSearchQuery should not be null");
}
if (mSearchType == SearchType.A) {
return RestAdapter().searchA(parameters);
} else {
return RestAdapter().searchB(parameters);
}
}
}
public abstract class ApiResponseLoader extends AsyncTaskLoader<ApiResponse> {
private final static String TAG = ApiResponseLoader.class.getSimpleName();
private ApiResponse mApiResponse;
private Exception mError;
public ApiResponseLoader(Context context) {
super(context);
}
public abstract ApiResponse tryLoadInBackground(Map<String, String> parameters) throws Exception;
#Override
protected void onStartLoading() {
if (mApiResponse != null) {
deliverResult(mApiResponse);
}
if (takeContentChanged() || mApiResponse == null) {
forceLoad();
}
}
#Override
protected void onForceLoad() {
super.onForceLoad();
}
#Override
protected void onStopLoading() {
cancelLoad();
}
#Override
public void onCanceled(ApiResponse data) {
// Attempt to cancel the current asynchronous load.
super.onCanceled(data);
}
#Override
protected void onReset() {
// Ensure the loader has been stopped.
onStopLoading();
// At this point we can release the resources associated with 'apps' if needed
if (mApiResponse != null) {
mApiResponse = null;
}
}
#Override
public void deliverResult(ApiResponse data) {
if (isReset()) {
// An async query came in while the loader is stopped. We don't need the result
if (data != null) {
onReleaseResources(data);
}
return;
}
if (mApiResponse != null) {
mApiResponse.mMeta = data.mMeta;
mApiResponse.mSampleList.addAll(data.mSampleList);
} else {
mApiResponse = data;
}
if (isStarted()) {
// If the loader is currently started, we can immediately deliver a result
super.deliverResult(mApiResponse);
}
}
public Exception getError() {
return mError;
}
public void setError(Exception mError) {
this.mError = mError;
}
}