Help us understand the problem. What is going on with this article?

Androidで遷移前にAPI通信を行うパターンの実装例

状況

最近、下記のような画面遷移のアプリを作ることがありました。

  1. 一覧画面を表示
  2. ユーザが任意の要素をタップ
  3. ローディングダイアログを表示し、API通信を始める
  4. APIの結果を受け取り、成功であれば詳細画面に遷移する

遷移元と遷移先が1対1であれば、遷移元にAPI実行処理を書いても問題ないとは思いますが、
多対1のケースもあったので、「遷移処理」を共通化するクラスを導入しました。

前提

今回のアプリでは、以下のライブラリを利用しました。

  • RxJava2
  • Retrofit2
  • Dagger2

また、API通信中には一律でローディングダイアログを表示し、
APIがエラーを返却した場合には、一律でエラーダイアログを表示することとしました。

もろもろ制約があり、Javaでの実装でした。。。

実装

※ 今回は、プロジェクトのコードを一部改ざんしながら貼ってるので、そのままでは動かない可能性があります。

まずは、APIエラーの場合に、一律でエラーメッセージを表示する部分。

public class ErrorConsumerFactory {
    private final LoginRouter loginRouter;

    @Inject
    public ErrorConsumerFactory(LoginRouter loginRouter) {
        this.loginRouter = loginRouter;
    }

    public Consumer<Throwable> create(Activity activity) {
        return throwable -> {
            if (throwable instanceof ConnectException || throwable instanceof UnknownHostException) {
                showMessageDialog(activity, activity.getString(R.string.message_network_error));
                return;
            }
            if (throwable instanceof SocketTimeoutException) {
                showMessageDialog(activity, activity.getString(R.string.message_timeout_error));
                return;
            }
            if (throwable instanceof HttpException) {
                Response response = ((HttpException) throwable).response();
                if (response.code() == 401) {
                    // authorization error
                    loginRouter.navigate(activity);
                    return;
                }
            }

            ErrorList errorList = convertFromThrowable(throwable);
            if (errorList == null) {
                showMessageDialog(activity, activity.getString(R.string.message_unknown_error));
                return;
            }
            String message = errorList.getErrorMessage();
            showMessageDialog(activity, message);
        };
    }

    @Nullable
    private ErrorList convertFromThrowable(Throwable throwable) {
        try {
            if (throwable instanceof HttpException) {
                Response response = ((HttpException) throwable).response();
                String responseString = response.errorBody().string();
                return // convert JSON to POJO by Gson
            }
            return null;
        } catch (IOException | JsonSyntaxException e) {
            // unknown JSON
            return null;
        }
    }

    private void showMessageDialog(Activity activity, String message) {
        // show message dialog.
    }
}

エラー時のExceptionをハンドリングして対応するメッセージのダイアログを出したり、
401エラーの場合は認証切れなのでログイン画面を出したり、
エラーの場合はJSONから表示用のメッセージのダイアログを出したり、
というのを共通処理化しました。

これをRouterクラスから利用します。
今回は、ECアプリにおけるカート画面を想定します。
カート画面は、メニューから遷移する以外にも、カート追加後に遷移したり、いくつかの遷移元があるかと思います。
カート一覧取得APIの実行が分散しないように、下記のRouterクラスを作成しました。

public class CartRouter extends AbstractRouter {
    private final ApiService apiService;
    private final ErrorConsumerFactory errorConsumerFactory;

    @Inject
    public CartRouter(ApiService apiService,
                      ErrorConsumerFactory errorConsumerFactory) {
        this.apiService = apiService;
        this.errorConsumerFactory = errorConsumerFactory;
    }

    public Disposable navigate(Activity activity, FragmentController fragmentController) {
        Fragment currentFragment = fragmentController.getCurrentFragment();
        if (currentFragment instanceof CartFragment) {
            return Disposables.empty();
        }

        showLoading(activity);

        return apiService.fetchCart()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnError(errorConsumerFactory.create(activity))
                .subscribe(cart -> {
                    Fragment fragment = CartFragment.newInstance(cart);
                    fragmentController.stackFragment(fragment);

                    hideLoading();
                }, throwable -> hideLoading());
    }
}

AbstractRouterに showLoadinghideLoading を定義し、ダイアログインスタンスを保持しています。
DaggerのDIにより、RetrofitのAPI serviceインスタンスと、先程の ErrorConsumerFactory をinjectします。

なんらかの条件(今回は、カート画面にいるのにカート画面に遷移しようとしてる場合)で遷移が不要の場合は、 Disposable.empty を返却することで、呼び元ではあまり意識する必要が無くなります。
doOnErrorerrorConsumerFactory.create の返却値を渡すことで、エラーの場合には先程のエラーハンドリングを行った上で、subscribeの第二引数が実行されます。
fetchCart が成功した場合には、取得できた情報を元にFragmentを作成し、スタックに追加しています。( FragmentController.stackFragment では、FragmentManagerを利用して add を行ったり、その前後の共通処理を行ったりしています。)

これを呼び出す側としては、下記のようになります。

public class MainActivity extends DaggerAppCompatActivity {
    @Inject
    CartRouter cartRouter;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    protected void onDestroy() {
        disposable.dispose();
        super.onDestroy();
    }

    // なんらかのボタンを押したときに呼ばれる
    public void onClickGoToCart() {
        disposable.add(
                cartRouter.navigate(TopActivity.this,
                        new FragmentController(getSupportFragmentManager()))
        );
    }
}

いろいろ省略してますが、DIによりinjectされた CartRouter のインスタンスの navigate を実行します。
返却された Disposable インスタンスは、 CompositeDisposable のインスタンスに追加しておくことで、Activityが破棄されたタイミングでちゃんと解除されます。

遷移元の画面としては、エラー処理や遷移の成否を気にすることなく、単純に遷移依頼を行っているだけとなり、すっきりします。

感想

今回、XXXRouterクラスを導入することで、API通信 -> 画面遷移を実装しました。
遷移に必要な処理をRouterクラスに押し込めることで、遷移元・遷移先の処理を単純化することができました。
同時に、すべての画面遷移を各Routerクラスに記述し、routerパッケージ以下にまとめることで、画面遷移を一箇所で管理することができました。

上記のような利点もありますが、悩みどころもありました。

まずは、そもそもAPI通信 -> 画面遷移という流れで良いのか?という点です。
多くのアプリでは、通信前に画面遷移を行ってしまい、遷移先の画面に応じたローディング表示を行っているかと思います。
例えば、一覧画面への遷移の場合はSwipeRefreshLayoutのローディングを表示させてみたり、詳細画面ではグレーの矩形を表示することで読み込み中を表したり。
こちらの方が、ユーザの体感速度は上がりそうです。
また、今回はローディング表示をモーダルで行っているため、API実行に長時間かかる場合には、ユーザーは待つしかなくなります。
これもユーザビリティを悪化させているとは思いますが、個別の画面での検討時間を減らし、実装の単純化のために仕方なく、といった状況でした。

ただ、今回の要件に対しては、Routerという層を追加するのは、ベターな選択肢だったと思います。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away