Wie gehe ich mit Fehlerzuständen mit LiveData um?

72

Das neue LiveDatakann in einigen Szenarien als Ersatz für die Observablen von RxJava verwendet werden. Doch im Gegensatz zu Observable, LiveDatahat keinen Rückruf für Fehler.

Meine Frage lautet: Wie soll ich mit Fehlern umgehen LiveData, z. B. wenn sie von einer Netzwerkressource unterstützt werden, die aufgrund einer nicht abgerufen werden kann IOException?

Kirill Rakhman
quelle
Ich denke, stackoverflow.com/a/45880925/2413303 ist hier die sauberste Variante.
EpicPandaForce

Antworten:

58

In einer der Beispiel-Apps von Google für Android-Architekturkomponenten wird das von LiveData ausgegebene Objekt in eine Klasse eingeschlossen, die einen Status, Daten und eine Nachricht für das ausgegebene Objekt enthalten kann.

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

Mit diesem Ansatz können Sie anhand des Status feststellen, ob ein Fehler aufgetreten ist.

Chris Cook
quelle
4
Probe war in Kotlin. Irgendjemand in Java?
Sudha
1
Achtung: Dies wurde in der Livedata-Unterstützung für Room nicht durchgeführt. Nicht behandelte Ausnahmen von der Datenbankabfrage führen zum Absturz der gesamten App.
Marcus Wolschon
Wie genau können Sie diesen Ansatz mit DataBinding kombinieren?
user123456789
39

Sie können MutableLiveDataein Haltermodell erweitern und erstellen, um Ihre Daten zu verpacken.

Dies ist Ihr Wrapper-Modell

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
    }
}

Dies ist Ihr erweitertes LiveData-Objekt

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());
    }

}

Und so benutzt du es

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

Und wie es beobachtet werden kann:

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;
        }
    }
Eliel Martinez
quelle
1
Wenn wir viele StateLiveData haben, müssen wir viele Handbücher haben !!
Ali Rezaiyan
2
Ich bin nicht in der Lage, die LiveDataaus der StateLiveDataKlasse zu
besetzen
Was ist stepIds im Switch?
Waqar Vicky
18

Schließen Sie die von LiveData zurückgegebenen Daten mit einer Art Fehlermeldung ab

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

// Jetzt in deiner LifecycleRegistryOwnerKlasse

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
    }

});

Fangen Sie Exceptionstattdessen eine oder werfen Sie sie. Verwenden Sie das Fehlerobjekt, um diese Daten an die Benutzeroberfläche zu übergeben.

MutableLiveData<DataWrapper<SomObject>> liveData = new...;

//On Exception catching:
liveData.set(new DataWrapper(null, new ErrorObject(e));
royB
quelle
Eine Frage, können wir das LiveDatain ein beobachtbares umwandeln Observable<LiveData<Model>>? Dann können wir die Fehler dort behandeln?
Santanu Sur
13

Ein anderer Ansatz ist die Verwendung von MediatorLiveDataQuellen LiveDataunterschiedlichen Typs. Dies gibt Ihnen eine Trennung von jedem Ereignis:

Zum Beispiel:

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 diesem Beispiel werden Laden und Fehler LiveDatabeobachtet, sobald MediatorLiveDataaktive Beobachter vorhanden sind.

Nikola Despotoski
quelle
Ich habe speziell nach diesem Ansatz gesucht und bin froh, dass ich genau diesen Ansatz gefunden habe (indem ich mehrere LiveData verwende und über MediatorLiveData in diese poste). : +1:
EpicPandaForce
Bitte beachten Sie, dass Flowables mehrere Elemente darstellen kann. In diesem Fall wird onComplete () niemals aufgerufen.
EpicPandaForce
1
@Nikola Despotoski, es ist spät, aber wenn Sie eine Frage haben, was passiert, wenn das Betriebssystem die Aktivität beendet und wiederherstellt. Während der Wiederherstellung des Flusses MediatorLiveDatawird das erneut beobachtet (es ist im viewModel noch aktiv). Das Problem ist, wenn es registriert / beobachtet wird Liefern Sie das, was beim letzten Mal in den liveData gepostet wurde. Wenn der letzte Beitrag ein Fehlerstatus war, kann die wiederhergestellte Aktivität die zuvor veröffentlichten Daten nicht abrufen, sodass die Benutzeroberflächenerfahrung nicht vor dem Beenden der Aktivität fortgesetzt werden kann. Wie gehe ich mit dem Betriebssystem um, das die Aktivität tötet / wiederherstellt MediatorLiveData?
Lannyf
@lannyf Werfen Sie einen Blick SingleLiveDatadarauf, um zu vermeiden, dass die neuen Beobachter das neueste Ergebnis erhalten. Das ist eine Möglichkeit, dies zu vermeiden.
Nikola Despotoski
@ Nikola Despotoski, danke für die Antwort. Dies databehebt jedoch nicht das Problem, dass wenn os die Aktivität wiederherstellt und die zuvor veröffentlichten nicht abgerufen werden (wenn der letzte Beitrag in liveData statenach dem Beitrag von liegt data). Wir könnten die statein den liveData ignorieren, wenn wir uns bei den liveData registrieren, aber wie können wir die Daten abrufen, um die vorherige UI-Erfahrung wiederherzustellen? Wenn wir zwei separate liveData-Kanäle hätten, einen für dataund einen für state, würde dieses Problem nicht auftreten. Wie können sie dann zu einem liveData kombiniert werden?
Lannyf
3

In meiner App musste ich RxJava Observables in LiveData übersetzen. Dabei musste ich natürlich den Fehlerzustand beibehalten. So habe ich es gemacht (Kotlin)

class LiveDataResult<T>(val data: T?, val error: Throwable?)

class LiveObservableData<T>(private val observable: Observable<T>) : LiveData<LiveDataResult<T>>() {
    private var disposable = CompositeDisposable()

    override fun onActive() {
        super.onActive()

        disposable.add(observable.subscribe({
            postValue(LiveDataResult(it, null))
        }, {
            postValue(LiveDataResult(null, it))
        }))
    }

    override fun onInactive() {
        super.onInactive()

        disposable.clear()
    }
}
Daniel Christopher
quelle
1
Das ist cool, aber warum haben Sie LiveDataReactiveStream nicht verwendet?
Hossein Shahdoost
LiveDataReactiveStreams.fromPublisher()behandelt keine Fehler, wie in der Dokumentation angegeben. Ein Rx-Fehler löst einen Fehler im Haupt-Thread aus und stürzt die App ab. Sie können die Fehler jedoch wahrscheinlich auch auf Empfangsebene in a einschließen und LiveDataResultdann verwenden, LiveDataReactiveStreams.fromPublisher()um sie in LiveData umzuwandeln.
BladeCoder
1

Nur eine Implementierung der Methode aus Chris Cooks Antwort:

Zuerst benötigen wir das Objekt, das Antwortdaten und Ausnahmen enthält:

/**
 * 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)
        }
    }
}

Und dann meine eigene Erfindung - AsyncExecutor .

Diese kleine Klasse macht 3 wichtige Dinge:

  1. Gibt ein praktisches Standard-LiveData-Objekt zurück.
  2. Anruf bereitgestellt Rückruf asynchron.
  3. Nimmt das Ergebnis des Rückrufs oder fängt eine Ausnahme ab und fügt sie in die LiveData ein.

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
        }
    }
}

Anschließend können Sie in Ihrem ViewModel LiveData erstellen, die das Ergebnis Ihres Rückrufs oder Ihrer Ausnahme enthalten:


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()
        }
    }
}

Und dann können Sie Ihre Daten und alle Ausnahmen in der Benutzeroberfläche abrufen:


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
    }
}

James Bond
quelle
0

Ich habe hier eine Filmsuch-App erstellt , in der ich verschiedene LiveDataObjekte verwendet habe, eines für die erfolgreiche Antwort aus dem Netzwerk und eines für die erfolglose:

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)
        }
    })
}
Ali Nem
quelle
Dazu müssen Sie 2 Beobachter von der Benutzeroberfläche anhängen
Gastón Saillén