问题
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});
回答1:
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;
}
}
回答2:
I'm working in a blog post for Error handling with LiveData in Kotlin. My solution uses 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
This class should provide you some light. Whenever I have the blog post ready, I can update this answer.
回答3:
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();
}
}
}
}
}
来源:https://stackoverflow.com/questions/46304042/architecture-components-retrofit-and-rxjava-2-error-handling