2015/05/11

Android : Mortar - Say Goodbye to unwanted lifecycle

Mortar library

この章はmotar - GitHubの内容を参考に筆者の解釈のもと多くの加筆・修正が加えられています.
Mortarについての正確な情報を確認するには上記のオリジナルページをご確認ください.

一言で言うと, MortarはAndroidの厄介なライフサイクルをいい具合に隠蔽してくれるライブラリだ.
Mortarは(Fragmentが目指した)モジュールユニットなView層を提供するアーキテクチャの基礎となり, DaggerによるObjectGraphと柔軟なScopeの概念によりActivityのライフサイクルからPresenterを分離しMVPを促進する.

MortarはDaggerのObjectGraphあるいはDagger2のComponentsをSystemServiceかのような形式で提供することもできる. もちろん不要であれば気にする必要はない. Daggerは完全に分離されている.

Mortarでは全ての管理をSingletonなMortarScopeが行う.
MortarScopeはMortarの肝になる概念で, オブジェクトにその名の通り”スコープ”を与える. 通常はApplicationやActivityといった範囲でスコープを割り当てる. 重要なのは”Activity”がスコープなのであって”Activityのライフサイクル”がスコープではないということだ.
Activityを実装するケースでは, Activityのライフサイクル単位をスコープとするのではなく, ConfigurationChangeやRecreateといったAndroid Framework都合のライフサイクルを超えた”Activity”をスコープとすることが多い.
つまり, より抽象的には”1画面”をスコープとしたいのだ. MotarScopeはこういったスコープの制御を提供してくれる.

MortarScopeは通常Applicationのコンテキストで保持する. あるいは独自の短命なスコープを持った一過性のオブジェクトとして定義することも可能だ. スコープはサブスコープとしてネストさせることもできる. ネストされたスコープはより上位のスコープを隠蔽し透過的に扱える.
例えば短命なスコープを持つウィザード画面へのオブジェクトグラフを形成するには次のように記述できる.

// ObjectGraphServiceは内部でgetSystemServiceを呼んでいるに過ぎない. 
ObjectGraphService.inject(getContext(), this)

Mortarが提供する強力なプリインサービスの1つにBundleServiceがある.
このサービスはView(あるいはActivity Contextにアクセスするオブジェクト)がActivityの持つBundleへ安全にアクセスできる機能を提供する.
MVPアーキテクチャのアプローチではBundleService上でPresenterを構築し持続性を持たせる.
前述の通りPresenterはViewからは完全に分離され, Activityのライフサイクル(ConfigurationChangeやRecreate)とは無縁の関係にあるべきだ. BundleServiceがViewへのシンプルで安全なアクセスを提供してくれる.

The Big Picture

アプリケーションはsingletonなMortarScopeを持つことが通常である.
MortarScopeはgetSystemServiceメソッドをデリゲートし次のように機能させる.

public class MyApplication extends Application {
  private MortarScope rootScope;

  @Override public Object getSystemService(String name) {
    if (rootScope == null) rootScope = 
        MortarScope.buildRootScope().build();

    return rootScope.hasService(name) ? 
        rootScope.getService(name) : super.getSystemService(name);
  }
}

これでsingletonなコアサービスとしてのスコープを定義できる.
スコープはサブスコープを持つことができる. サブスコープはスコープの登録/解除を対で呼ぶ責任を負う.

  • Scoped.onExnterScope(MortarScope)
  • Scoped.onExitScope(MortarScope)

スコープの中でDaggerが持つObjectGraphのように他のサービスを提供するには次のように記述する.

@Override public Object getSystemService(String name) {
    if (rootScope == null) {
        rootScope = MortarScope.buildRootScope()
          .with(ObjectGraphService.SERVICE_NAME, 
              ObjectGraph.create(new RootModule()))
          .build();
    }

    return rootScope.hasService(name) ? 
        rootScope.getService(name) : super.getSystemService(name);
}

ContextをこのObjectGraphに登録するにはMortarScopeにプリインされているサービスObjectGraphServiceを使えば容易にできる.

public class MyView extends LinearLayout {
  @Inject SomeService service;

  public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);
    ObjectGraphService.inject(context, this);
  }
}

MortarはActivityのライフサイクルに合うサブスコープを形成するにはActivityのgetSystemServiceメソッドでこれを構築する. ActivityでMortarScopeを構築する際にはBundleServiceRunnerを設定しておく. ActivityのConfigurationChangeやRecreateによる厄介な問題をBundleServiceRunnerは手助けする.
BundleServiceRunnerのBundleに必要な情報を詰め込み, Activityが然るべきタイミングでBundleServiceRunnerのonCreateとonSaveInstanceStateを呼び出すことで情報のsave/restoreを可能にする.

public class MyActivity extends Activity {
  private MortarScope activityScope;

  @Override public Object getSystemService(String name) {
    MortarScope activityScope = 
        MortarScope.findChild(getApplicationContext(), getScopeName());

    if (activityScope == null) {
      activityScope = MortarScope.buildChild(getApplicationContext(), getScopeName())
          .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner())
          .withService(HelloPresenter.class.getName(), new HelloPresenter())
          .build();
    }

    return activityScope.hasService(name) ?
        activityScope.getService(name) : super.getSystemService(name);
  }

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState);
    setContentView(R.layout.main_view);
  }

  @Override protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    BundleServiceRunner.getBundleServiceRunner(this).onSaveInstanceState(outState);
  }

  @Override protected void onDestroy() {
    if (isFinishing()) {
      MortarScope activityScope = findChild(getApplicationContext(), getScopeName());
      if (activityScope != null) activityScope.destroy();
    }

      super.onDestroy();
  }
}

View.onSaveInstanceStateで求められるようなParcellable化の手間はもはや必要なくなる.
MortarScopeを得た今, 多くのオブジェクトがBundleServiceにアクセスできる状態である.
つまり, save/restoreはBundleServiceRunnerに任せることができる状態である.

Reference.

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.