How to handle error states with LiveData?

后端 未结 7 609
暗喜
暗喜 2020-12-07 14:10

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

相关标签:
7条回答
  • 2020-12-07 14:46

    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 (stepIds.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;
            }
        }
    
    0 讨论(0)
  • 2020-12-07 14:47

    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)
            }
        })
    }
    
    0 讨论(0)
  • 2020-12-07 14:47

    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
        }
    }
    
    
    0 讨论(0)
  • 2020-12-07 14:56

    In one of Google's sample apps for Android Architecture Components they wrap the LiveData emitted object in a class that can contain a status, data, and message for the emitted object.

    https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt

    With this approach you can use the status to determine if there was an error.

    0 讨论(0)
  • 2020-12-07 14:57

    Wrap the Data that you return from LiveData with some sort of error Messaging

    public class DataWrapper<T>T{
        private T data;
        private ErrorObject error; //or A message String, Or whatever
    }
    

    //Now in your LifecycleRegistryOwner Class

    LiveData<DataWrapper<SomeObjectClass>> result = modelView.getResult();
    
    result.observe(this, newData ->{
        if(newData.error != null){ //Can also have a Status Enum
            //Handle Error
        }
        else{
           //Handle data
        }
    
    });
    

    Just Catch an Exception instead or throwing it. use the error Object to pass this Data to the UI.

    MutableLiveData<DataWrapper<SomObject>> liveData = new...;
    
    //On Exception catching:
    liveData.set(new DataWrapper(null, new ErrorObject(e));
    
    0 讨论(0)
  • 2020-12-07 15:04

    Another approach is to use MediatorLiveData that will take sources of LiveData of different type. This will give you separation of each event:

    For example:

    open class BaseViewModel : ViewModel() {
        private val errorLiveData: MutableLiveData<Throwable> = MutableLiveData()
        private val loadingStateLiveData: MutableLiveData<Int> = MutableLiveData()
        lateinit var errorObserver: Observer<Throwable>
        lateinit var loadingObserver: Observer<Int>
        fun <T> fromPublisher(publisher: Publisher<T>): MediatorLiveData<T> {
            val mainLiveData = MediatorLiveData<T>()
            mainLiveData.addSource(errorLiveData, errorObserver)
            mainLiveData.addSource(loadingStateLiveData, loadingObserver)
            publisher.subscribe(object : Subscriber<T> {
    
                override fun onSubscribe(s: Subscription) {
                    s.request(java.lang.Long.MAX_VALUE)
                    loadingStateLiveData.postValue(LoadingState.LOADING)
                }
    
                override fun onNext(t: T) {
                    mainLiveData.postValue(t)
                }
    
                override fun onError(t: Throwable) {
                    errorLiveData.postValue(t)
                }
    
                override fun onComplete() {
                    loadingStateLiveData.postValue(LoadingState.NOT_LOADING)
                }
            })
    
            return mainLiveData 
        }
    
    }
    

    In this example loading and error LiveData will start being observed once the MediatorLiveData will have active observers.

    0 讨论(0)
提交回复
热议问题