首页 > 解决方案 > 在 MVVM 架构中处理来自 api 端点的错误的最佳实践是什么?

问题描述

我的目标(也是问题)是做集中式错误处理。在大多数情况下,每个 API 端点的错误都将以相同的方式处理,因此我不希望有重复或大量if else语句。

我的应用程序架构对应于developer.android.com中描述的架构

在此处输入图像描述

因此,这意味着我应该将错误从repovia传递viewModelUI layer (Activity/Fragment),以便从该层进行 UI 更改。

我的代码中的一些小部分:

myService.initiateLogin("Basic " + base64, authBody)
                .enqueue(new Callback<UserTokenModel>() {
                    @Override
                    public void onResponse(Call<UserTokenModel> call, Response<UserTokenModel> response) {
                        userTokenModelMutableLiveData.setValue(response.body());
                    }

                    @Override
                    public void onFailure(Call<UserTokenModel> call, Throwable t) {
                        // TODO better error handling in feature ...
                        userTokenModelMutableLiveData.setValue(null);
                    }
                });

假设我们需要为每个onFailure(...)方法调用显示 Toast,或者每个 api 调用何时errorBody不在方法null中。onResponse(...)

那么,在保持架构不变的同时进行“集中式”错误处理的建议是什么?

标签: androidmvvmerror-handlingretrofit2rx-java2

解决方案


通用改造回调

要将 repo 层错误传递给 UI,您可以将模型类与错误一起包装到通用组合模型中,如下所示:

class Resource<T> {

    @Nullable private final T data;
    @Nullable private final Throwable error;

    private Resource(@Nullable T data, @Nullable Throwable error) {
        this.data = data;
        this.error = error;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(data, null);
    }

    public static <T> Resource<T> error(@NonNull Throwable error) {
        return new Resource<>(null, error);
    }

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

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

在一个单独的帮助器类中,我们定义了一个通用的 Retrofit 回调来处理错误,并将 API 结果转换为资源。

class ResourceCallback {

    public static <T> Callback<T> forLiveData(MutableLiveData<Resource<T>> target) {

        return new Callback<T>() {
            @Override
            public void onResponse(Call<T> call, Response<T> response) {
                if (!response.isSuccessful() || response.body() == null) {
                    target.setValue(Resource.error(convertUnsuccessfulResponseToException(response)));
                } else {
                    target.setValue(Resource.success(response.body()));
                }
            }

            @Override
            public void onFailure(Call<T> call, Throwable t) {
                // You could examine 't' here, and wrap or convert it to your domain specific exception class.
                target.setValue(Resource.error(t));
            }
        };

    }

    private static <T> Throwable convertUnsuccessfulResponseToException(Response<T> response) {
        // You could examine the response here, and convert it to your domain specific exception class.
        // You can use
        response.errorBody();
        response.code();
        response.headers();
        // etc...

        return new LoginFailedForSpecificReasonException(); // This is an example for a failed login
    }
}

您可以在存储库层中调用 API 的所有位置使用此通用 Retrofit 回调。例如:

class AuthenticationRepository {

    // ...

    LiveData<Resource<UserTokenModel>> login(String[] params) {

        MutableLiveData<Resource<UserTokenModel>> result = new MutableLiveData<>();
        myService.initiateLogin("Basic " + base64, authBody).enqueue(ResourceCallback.forLiveData(result));

        return result;
    }
}

装饰观察者

现在,您有了使用 Retrofit API 的通用方式,并且有了包装模型和错误的 LiveData。此 LiveData 从 ViewModel 到达 UI 层。现在我们用通用错误处理来装饰实时数据的观察者。

首先,我们定义一个可以实现的 ErrorView 接口,但是您想向用户显示错误。

interface ErrorView {
    void showError(String message);
}

这可以通过显示 Toast 消息来实现,但是您可以使用 Fragment 自由地实现 ErrorView 并使用 Fragment 上的错误消息做任何您想做的事情。我们使用一个单独的类,以便可以在每个 Fragment 中使用相同的类(使用组合而不是继承作为最佳实践)。

class ToastMessageErrorView implements ErrorView {

    private Context context;

    public ToastMessageErrorView(Context context) {
        this.context = context;
    }

    @Override
    public void showError(String message) {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show();
    }
}

现在我们实现了观察者装饰器,它包装了一个装饰过的观察者并用错误处理来装饰它,在发生错误时调用 ErrorView。

class ResourceObserver {

    public static <T> Observer<Resource<T>> decorateWithErrorHandling(Observer<T> decorated, ErrorView errorView) {

        return resource -> {
            Throwable t = resource.getError();
            if (t != null) {
                // Here you should examine 't' and create a specific error message. For simplicity we use getMessage().
                String message = t.getMessage();

                errorView.showError(message);
            } else {
                decorated.onChanged(resource.getData());
            }

        };
    }
}

在您的片段中,您可以像这样使用观察者装饰器:

class MyFragment extends Fragment {

    private MyViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        viewModel.getUserToken().observe(this, ResourceObserver.decorateWithErrorHandling(
                userTokenModel -> { 
                    // Process the model
                }, 
                new ToastMessageErrorView(getActivity())));
    }

}

PS 有关将 API 与本地数据源相结合的更详细的资源实现,请参阅此内容。


推荐阅读