Androidアプリ開発でModelを作るようにしたら少し幸せになった話

こんにちは。Androiderの梅津です。

今日はAndroidアプリの開発をするにあたって、日頃意識している設計の話をしたいと思います。

設計と言っても、Android Clean ArchitectureだとかDDDみたいな話は出てきません。

そんな大それた話ではなく、MV何とかパターンにおけるところのModelっていうのが結構重要だよ、という話をしたいと思います。

それではいってみましょー。


Modelとは?

単にModelといっても意味するものは人によって違ったりしますよね。

ここで言うModelは、次のスライドを参考にしたものです。

ざっくりまとめると、Modelは下記のような特徴を持っています。 * ActivityやFragmentよりも寿命が長い * UIの表示に必要なデータの取得・提供・保持などを行なう * "通知"によってデータに変更があったことを伝える

JSONをマッピングするJavaクラスのことをModelと呼ぶ事もありますが、この記事ではそういったもののことをEntityと呼ぶことにします。

Modelがなかった頃の問題点

そもそもなんでModelが必要になるのでしょうか?

Fragmentが直接サーバーへ通信してデータを取ってきたっていいですよね。

例えばこんなコードです。

public class SampleFragment extends Fragment {

    ...省略

    @Override
    public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        SampleApi.instance().fetchData(new SampleApi.Callback() {
            @Override
            public void onSuccess(final List<SampleEntity> entities) {
                // Viewを更新したり、Contextが必要になってgetActivity()してみたり
            }
        });
    }
}

このコードの問題は、コールバックが呼ばれたときにFragmentがすでに破棄されている可能性があるというところです。

Fragmentが破棄された状態でコールバックが呼ばれてしまうと、 Viewの更新などをしようとしたときにNullPointerExceptionが発生してクラッシュしてしまいます。

これを避けるにはコールバックの先頭でFragmentの状態を確認する必要があります。

@Override
public void onSuccess(final List<SampleEntity> entities) {
    // デタッチされてたら処理を中断
    if (isDetached() || getActivity() == null) return;   
    ...
}

確かにこれでクラッシュを避けることができるようになりました。

しかし非同期処理を書くたびにFragmentの状態を確認するようなコードを書くのは面倒です。

それに、もしデタッチされているかの確認を忘れたままアプリをリリースしてしまったら…と考えると気が気ではありません。

もう少し気軽に非同期処理を扱える仕組みが必要です。

Modelを使ったコードに書き換える

この問題はModelを使うことで解決できます。

それではModelを使ったコードに書き換えてみましょう。

public class SampleFragment extends Fragment {

    ...

    @Override
    public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        final SampleModel model = SampleModel.instance();
        final List<SampleEntity> samples = model.getSamples();
        if (samples == null) {
            model.loadSamples();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        // 通知を受け取れるように登録
        EventBus.getDefault().register(this);
    }

    @Override
    public void onPause() {
        super.onPause();
        // 通知の登録解除
        EventBus.getDefault().unregister(this);
    }

    @Subscribe
    public void onLoadSuccess(final SamplesLoadSuccessEvent event) {
        // データの読み込みに成功するとModelから通知されてこのメソッドが呼ばれる
        updateViews(SampleModel.instance().getSamples());
    }

    private void updateViews(final List<SampleEntity> samples) {
        // Viewの更新
    }
}

/**
 * Model側の実装例
 */
public class SampleModel {

    ...

    private List<SampleEntity> mSampleEntities;

    public List<SampleEntity> getSamples() {
        return mSampleEntities;
    }

    public void loadSamples() {
        SampleApi.instance().fetchSamples(new SampleApi.Callback() {
            @Override
            public void onSuccess(final List<SampleEntity> samples) {
                mSampleEntities = samples;  // メモリにキャッシュしておく
                EventBus.getDefault().post(new SamplesLoadSuccessEvent());
            }
        });
    }
}

イメージはこんな感じです。

実務ではModelをinterfaceにして実装を切り替えられるようにしていますが、今回は端折りました。

データの変更通知にはEventBusを使っています。

onResumeで通知を受け取る準備をし、onPauseで通知の受け取りを解除しています。

こうすることでonLoadSuccessが呼ばれるのはFragmentがユーザーの目に触れているときだけになります。

破棄されている場合は通知がこないため、安心してViewの更新を行うことができるようになりました。

僕が実務で関わっているプロジェクトでは立ち上げの段階からこの設計を意識してやってきました。

そのおかげか、クラッシュ数は低く抑えられています。(1日平均5~6件くらい)

最後に

さんざんModelと呼んできましたが、他の人であれば違った呼び方をしたりするでしょう。

なので呼び方についてアレコレ言う気はありません。

重要なのは、

  1. Activity/Fragmentよりも寿命が長いクラスを用意し、そいつに非同期処理を行わせる
  2. 非同期処理が終わった事を"通知"を使って伝える。(コールバックではダメ)

ということです。

この2点を守って、クラッシュしづらいアプリをユーザーに届けていきましょう。

それでは、楽しいAndroidライフを。