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を持つことが通常である.
MortarScopegetSystemServiceメソッドをデリゲートし次のように機能させる.

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のonCreateonSaveInstanceStateを呼び出すことで情報の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.