2015/05/10

Android : Model-View-PresenterとAndroid

この記事は
- よりよい実装方法がある
- 説明に不足がある
といった理由から取り下げます.

アーキテクチャについての記事は追って書き直します.

Model - View - Presenter と Android

  • Viewは, データの出力を行い, ユーザ操作を受け付けPresenterにこれを伝搬するUI層. AndroidではActivity, Fragment, Viewに相当する.
  • Modelは, SQLite3やWebAPI, SharedPreferenceなどのレポジトリ.
  • Presenterは, ModelとViewのブリッジ役を担う. Modelの内容を整形しViewにそれを伝搬する. またViewへの入力を適切なModelに伝搬する.

Androidアプリケーションのプログラミングで厄介な問題の1つにActivity/Fragment/Viewといった固有のライフサイクルをもつオブジェクトでバックグラウンドタスクを管理することが挙げられる.
Model-View-ControllerアーキテクチャやModel-Viewアーキテクチャは, ModelとView/Controllerが関係を持つ. AndroidではController/ViewにあたるActivity, Fragment, Viewがライフサイクルを持っている.
Modelとの通信においてはController/Viewがそのライフサイクルを終え, 不意に破棄, 再生成されることを念頭に設計しなければならない.

MVCは古き良きアーキテクチャであったが, Androidでは開発者が気にしなければならない課題が少なくない.
昨今のアプリケーションのFEPはキーボードやマウス、ジョイスティックコントローラといった類いのものではなくなっている.
マテリアルデザインはリッチなユーザインタフェースを要求し, より複雑・高度化された”display”と”input”はViewとControllerの境界を曖昧にした.
MVCでは無理がある. 無理矢理MVCを適用して, その結果ViewだかControllerだかハッキリしないActivityやFragmentがGod object化してしまうのも無理はない.

Presenterはバックグラウンドタスクを管理する. またPresenterはActivity, Fragment, Viewのライフサイクルから分離させる. これにより前述の厄介な問題を排除し, KISSの原則を促進する.
また, AndroidアプリケーションではしばしばActivityがGod object化する傾向にあるが, Presenterへの適切な責務分担がこれを解消する.

PresenterはFragmentではない. Presenterは前述の通り厄介なライフサイクルからは分離されたオブジェクトである. AndroidにはConfigurationChangeの概念とActivityのRecreationの概念があることを忘れてはいけない. 面倒で推奨もされていないギミックでこれらを避けようとすることはできるが問題を見え辛くしているにすぎない.
FragmentやLoaderの類いはConfigurationChangeの課題を多少解消してくれる(retain-instance)が根本的な解決ではないし, ActivityのRecreationには対応できていない.
またFragmentやLoaderがTestabilityの面で優れていないのも重要なポイントである.

- ConfigChange ActivityRecreate ProcessRestart
Activity, Fragment, View save/restore save/restore save/restore
Fragment.setRetainInstance(true) no change save/restore save/restore
Static variables and threads no change no change reset

PresenterはGod object化しないように適切に責務分割される必要がある(でないと従来のActivityと同じ轍を踏むことになる).
PresenterはFragmentよりも容易に責務分割できるはずだ. なぜならPresenterは厄介なライフサイクルの呪縛から解放されているのだから.

Presenter Example

View

ViewはPresenterへの参照を持ち, Presenterを生成し自身と関連づける.
PresenterのライフサイクルをActivityのそれから切り離すためにPresenterはstaticフィールドとして宣言される.
注意するべきポイントはPresenterはtakeViewメソッドによりViewへの参照を持つことになるという点である.
View, つまりはActivity,Fragment,Viewへの参照がstaticフィールドに保持されるため, 上手く参照を破棄しないとメモリリークを引き起こす.
ライフサイクルが終わりを迎えるタイミングでPresenterに自身の破棄を要求し, かつstaticフィールドの参照をnullに設定する.

public class MainActivity extends ActionBarActivity {

    private static MainPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (presenter == null) {
            presenter = new MainPresenter();
        }
        presenter.takeView(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.takeView(null);
        if (isFinishing()) {
            presenter = null;
        }
    }

    public void onUpdateView(final CuteModel model) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                TextView v = ((TextView) findViewById(R.id.text));
                v.setText(v.getText() + ", " + model.getValue());
            }
        });
    }
}

Presenter

PresenterはModelとViewへの参照を持つ. Viewへの参照はView自身からtakeView()により関連づけされる. ModelはPresenterが生成し, 適切なタイミングでViewに変更内容を伝搬(updateView)する.
PresenterはViewのライフサイクルに左右されない. PresenterとViewの関係はModelの変更を伝搬する先がいる(view != null)かいない(view == null)かに留まる.
厄介なライフサイクルから解放されたPresenterは非同期ローディングや突発的な外部イベントにも柔軟に対応できる可能性を得る.
この例ではCuteModelが非同期にvalueをロードし結果をコールバックしてくるが, “呼び出されたにも関わらずコールバック先が破棄された!”あるいは”コールバックを受け取れる状態ではない”といった不可思議な状態には陥らないし, 無用なキャンセラレーションの実装も必要最小限で済む. もはやAndroidアプリケーションで非同期ローディングは怖くなくなった.

public class MainPresenter {
    private CuteModel model;
    private MainActivity view;

    public MainPresenter() {
        model = new CuteModel();
        model.query(new Listener() {
            @Override
            public void callback() {
                updateView();
            }
        });
    }

    public void takeView(MainActivity view) {
        this.view = view;
        updateView();
    }

    private void updateView() {
        if (view != null) {
            view.onUpdateView(model);
        }
    }
}

Model

Modelはビジネスロジックに集中できる. Listenerは存在する限り健全な状態であることが保証されている.

public class CuteModel {
    public interface Listener {
        void callback();
    }

    private int value = 0;

    public void query(final Listener listener) {
        Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
            @Override
            public void run() {
                value = 100;
                listener.callback();
            }
        }, 5000, TimeUnit.MILLISECONDS);
    }

    public int getValue() {
        return value;
    }
}

Next step.

必要なアーキテクチャは揃ったが, PresenterとViewの間ではお決まりのやり取りがその数だけ存在する. コピーコードを避けるためにもここでKISSを推進するツールの導入を考えてみる.

それにはMortarとDaggerを使う選択肢がある. MvpWithRecyclerView

Reference.