How to handle error states with LiveData?

Viewed 38818

The new LiveData can be used as a replacement for RxJava's observables in some scenarios. However, unlike Observable, LiveData has no callback for errors.

My question is: How should I handle errors in LiveData, e.g. when it's backed by some network resource that can fail to be retrieved due to an IOException?

7 Answers

You can extend from MutableLiveData and create a holder Model to wrap your data.

This is your Wrapper Model

public class StateData<T> {

    @NonNull
    private DataStatus status;

    @Nullable
    private T data;

    @Nullable
    private Throwable error;

    public StateData() {
        this.status = DataStatus.CREATED;
        this.data = null;
        this.error = null;
    }

    public StateData<T> loading() {
        this.status = DataStatus.LOADING;
        this.data = null;
        this.error = null;
        return this;
    }

    public StateData<T> success(@NonNull T data) {
        this.status = DataStatus.SUCCESS;
        this.data = data;
        this.error = null;
        return this;
    }

    public StateData<T> error(@NonNull Throwable error) {
        this.status = DataStatus.ERROR;
        this.data = null;
        this.error = error;
        return this;
    }

    public StateData<T> complete() {
        this.status = DataStatus.COMPLETE;
        return this;
    }

    @NonNull
    public DataStatus getStatus() {
        return status;
    }

    @Nullable
    public T getData() {
        return data;
    }

    @Nullable
    public Throwable getError() {
        return error;
    }

    public enum DataStatus {
        CREATED,
        SUCCESS,
        ERROR,
        LOADING,
        COMPLETE
    }
}

This is your extended LiveData Object

public class StateLiveData<T> extends MutableLiveData<StateData<T>> {

    /**
     * Use this to put the Data on a LOADING Status
     */
    public void postLoading() {
        postValue(new StateData<T>().loading());
    }

    /**
     * Use this to put the Data on a ERROR DataStatus
     * @param throwable the error to be handled
     */
    public void postError(Throwable throwable) {
        postValue(new StateData<T>().error(throwable));
    }

    /**
     * Use this to put the Data on a SUCCESS DataStatus
     * @param data
     */
    public void postSuccess(T data) {
        postValue(new StateData<T>().success(data));
    }

    /**
     * Use this to put the Data on a COMPLETE DataStatus
     */
    public void postComplete() {
        postValue(new StateData<T>().complete());
    }

}

And this is how you use it

StateLiveData<List<Book>> bookListLiveData;
bookListLiveData.postLoading();
bookListLiveData.postSuccess(books);
bookListLiveData.postError(e);

And how it can be observed:

private void observeBooks() {
        viewModel.getBookList().observe(this, this::handleBooks);
    }
  
    private void handleBooks(@NonNull StateData<List<Book>> books) {
      switch (books.getStatus()) {
            case SUCCESS:
                List<Book> bookList = books.getData();
                //TODO: Do something with your book data
                break;
            case ERROR:
                Throwable e = books.getError();
                //TODO: Do something with your error
                break;
            case LOADING:
                //TODO: Do Loading stuff
                break;
            case COMPLETE:
                //TODO: Do complete stuff if necessary
                break;
        }
    }

Just some implementation of the method from Chris Cook's answer:

At first, we need the object that will contain response data and exceptions:

/**
 * A generic class that holds a value with its loading status.
 *
 * @see <a href="https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt">Sample apps for Android Architecture Components</a>
 */
data class Resource<out T>(val status: Status, val data: T?, val exception: Throwable?) {
    enum class Status {
        LOADING,
        SUCCESS,
        ERROR,
    }

    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(exception: Throwable): Resource<T> {
            return Resource(Status.ERROR, null, exception)
        }

        fun <T> loading(): Resource<T> {
            return Resource(Status.LOADING, null, null)
        }
    }
}

And then my own invention - AsyncExecutor.

This small class do 3 important things:

  1. Return standard convenient LiveData object.
  2. Call provided callback asynchronously.
  3. Takes the result of the callback or catch any exception and put it to the LiveData.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

class AsyncExecutor {
    companion object {
        fun <T> run(callback: () -> T): LiveData<Resource<T>> {
            val resourceData: MutableLiveData<Resource<T>> = MutableLiveData()

            Thread(Runnable {
                try {
                    resourceData.postValue(Resource.loading())
                    val callResult: T = callback()
                    resourceData.postValue(Resource.success(callResult))
                } catch (e: Throwable) {
                    resourceData.postValue(Resource.error(e))
                }
            }).start()

            return resourceData
        }
    }
}

Then you can create a LiveData in your ViewModel, contains the result of your callback or exception:


class GalleryViewModel : ViewModel() {
    val myData: LiveData<Resource<MyData>>

    init {
        myData = AsyncExecutor.run {
            // here you can do your synchronous operation and just throw any exceptions
            return MyData()
        }
    }
}

And then you can get your data and any exceptions in the UI:


class GalleryFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        galleryViewModel = ViewModelProviders.of(this).get(GalleryViewModel::class.java)
       
       // ...

        // Subscribe to the data:
        galleryViewModel.myData.observe(viewLifecycleOwner, Observer {
            when {
                it.status === Resource.Status.LOADING -> {
                    println("Data is loading...")
                }
                it.status === Resource.Status.ERROR -> {
                    it.exception!!.printStackTrace()
                }
                it.status === Resource.Status.SUCCESS -> {
                    println("Data has been received: " + it.data!!.someField)
                }
            }
        })

        return root
    }
}

I have built a movie search app here in which I have used to different LiveData objects, one for the successful response from the network and one for the unsuccessful:

private val resultListObservable = MutableLiveData<List<String>>()
private val resultListErrorObservable = MutableLiveData<HttpException>()

fun findAddress(address: String) {
    mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
        override fun onSuccess(t: List<MainModel.ResultEntity>) {
            entityList = t
            resultListObservable.postValue(fetchItemTextFrom(t))
        }

        override fun onError(e: Throwable) {
            resultListErrorObservable.postValue(e as HttpException)
        }
    })
}
Related