この Post について
- 少し前に dagger2 の scope についての post をしました
- ひとつ良い活用方法を見つけたので、それを紹介していきます
- こんな人に読んでもらえると良いなぁ
- Activity 再生成に四苦八苦している
- Dagger2 使ってるけど (用語はすんなり分かるけど) 活用の仕方がよく分からん
解決したいなぁと思ったこと
- 複数の画面をまたぎながら様々な情報を入力し、完成した data model をサーバに送信 という機能を実装しようとした時、データの引き回しが面倒すぎるのでもっとシンプルな問題に落としたい
- その中で、Model、ViewModel、UI 間に存在している依存関係を実装上きちんと整理したい
- 特に 生存期間/ライフサイクル をきちんと意識した実装をしたい (ムダに長生きさせない)
先に結論とお断り
- Dagger2 を活用することで⬆のほとんどは実現できましたが、若干泥臭い部分が残りました
- 多分 Redux とか Flux みたいな話かもしれないけど、そこは敢えて気にしてないスタンスで、あくまで "具体例 (何をどうやって解決するのか)" に主眼を置いて書こうと思います
- MVVM パターンでの実装を前提としてます
今回取り上げる具体例
- ワタクシ、バンドのイベントを取りまとめるアプリをコツコツ作ってまして
- その中の イベント作成画面 を具体例として取り上げようと思います
1. 投稿画面 | 2. 場所を選択する (会場選択ボタンをおすと遷移) |
---|---|
作った事がある方は分かると思いますが、結構難しいですよね (この例ならサブ画面は一つだから簡単かもしれないけど…)。
Activity
であれば Intent
にデータを詰めたり、onSaveInstanceState()
で回転に耐えたり、setResult()
したり。Fragment
ならどうしますか? getActivity()
を cast してデータを Activity
に詰めこんでいくなどですかね。
色々な実装方法があり、実装したい機能によって実装方法が変わるので、割と複雑さが生まれやすいポイントかと思います。
いろいろ厄介なので "そもそも" を考える
と、この話って要するに
-
1
の画面が始まってから終わるまでの間に一つの model が存在し - それを複数の画面から少しずつ値を詰めていき、
- "イベントを作成" が押されたところでその model をサーバへ送信する
という話なんですよね。
そう考えるとデータのライフサイクルが見えてきて、この話には 3 つのライフサイクルが存在していると解釈しました。短い順に並べると下記の通りです。
a. 各サブ画面が表示されてから消えるまで (各画面の ViewModel <- いわゆる MVVM の VM です)
b. 投稿開始から完了まで (イベントの model など)
c. アプリケーション全体が開始してから終了するまで (アプリ全体に関係するデータ、例えば Auth)
そして、a
のライフサイクルを持つ人たちは b
のライフサイクルの中にある model を触りますので、a
→ b
の方向の依存関係が見えてきます。b
-> c
の依存関係もあります。
実装上の課題に落としこむ
以上より、この画面の実装を行う際に考えると良さそうなことは下記の通りです。
- 3 つのライフサイクルを実現する
- それぞれの依存関係を実現する
これらができれば Activity
や Fragment
で複雑な事を考えず、シンプルな実装に持っていくことが出来そうです。
どう実装するか
- ここまで述べてきた通り、この話は 依存関係の解決 が一つの重要なポイントになります
- 今回は Dagger2 (https://google.github.io/dagger/) を主軸として使っていくことにしました
- Dagger2 は DI ライブラリの中でも特に著名なものですし、そんな Dagger2 で考えていることが実現できるか? という事に興味があったためです
では、自分が考えた進め方を一つ一つ順に解説します。
まずは UI 部分には全く手を付けず、ViewModel 以下の実装と依存関係の解決から進めます。
ライフサイクルに名前をつける
-
こちら でも述べたとおり、Dagger2 の上での
Component
のライフサイクルはScope
というものです - 今回実現したい 3 つの
Scope
に、それぞれ下記のような名前を付けて実装しましょう
a. それぞれ (この例だと2つ) の画面が表示されてから消えるまで (@ViewModelScope)
b. 投稿開始から完了まで (@EventRegisterScope)
c. アプリケーション全体が開始してから終了するまで (@AppScope)
- 実装、といってもここではただアノテーションをつくるだけです
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface EventRegisterScope {
}
- ところで
@ViewModelScope
を "画面が表示されてから消えるまで" としましたが、Activity/Fragment
のライフサイクルではなくあくまでユーザから見た時の "表示されてから消えるまで" であるところに注意してください (つまり、消える =onDestroy
ではない)
Component を定義
- 今回用意する Component と scope は下図の通りで、主な責務を解説します
AppComponent
- このアプリでは Firebase 関連のデータを provide していますが、この話と直接関係ないので割愛します
EventRegisterComponent
- イベント投稿の model を管理していて、イベント投稿機能の最中にこの component を inject することで常に唯一の model インスタンスにアクセスできます
- またどの画面が active かというイベントを発信する model もここに置きます
EventRegisterTopComponent
- イベント投稿の top 画面 (キャプチャの 1 の画面) に紐づく component です
- その画面の ViewModel を provide し、途中で Activity/Fragment の再生成が起きても同じ ViewModel を返します
- その ViewModel は
EventRegisterComponent
が provide する model へアクセスし、ユーザの入力を model へ反映させていく責務を持ちます
PlaceSelectComponent
- イベント投稿の 会場選択画面 (キャプチャの 2 の画面) に紐づく component です
- あとは
EventRegisterComponent
と同様です
Component 間の依存を解決する
-
EventRegisterComponent
からAppComponent
への依存の解決を行うところを取り上げて解説します
依存があることを宣言
-
dependencies
属性を記述し、AppComponent
の依存を宣言します - そして、
EventRegisterComponent
を介してAppComponent
から取り出したいデータを宣言しておきます- この場合は
AppData provideAppData
のところ - (少し話が逸れますが) これによく似た
SubComponent
ではこんな事をする必要は無いですが、dependencies
の場合は必要みたいです
- この場合は
@EventRegisterScope
@Component(
modules = {
EventRegisterComponent.EventRegisterModule.class
},
dependencies = {
AppComponent.class
}
)
public interface EventRegisterComponent {
AppData provideAppData();
AppComponent
への依存を解決
- 宣言を行ったことによって、
EventRegisterComponent
へのAppComponent
が注入できるようになります (builder にappComponent
のインスタンスが渡せるようになる)
eventRegisterComponent = DaggerEventRegisterComponent.builder()
.appComponent(MyApplication.appComponent(context))
.build();
Component の依存関係グラフを実装する
- 前述の通り依存の宣言によって注入が可能になったので、こんな実装が考えられます
public class EventRegisterGraph {
private final EventRegisterComponent eventRegisterComponent;
private final ViewModelComponentsHolder viewModelComponents;
public EventRegisterGraph(AppComponent appComponent) {
eventRegisterComponent = DaggerEventRegisterComponent.builder()
.appComponent(appComponent)
.build();
viewModelComponents = new ViewModelComponentsHolder(eventRegisterComponent);
}
public EventRegisterComponent eventRegisterComponent() {
return eventRegisterComponent;
}
public ViewModelComponentsHolder viewModelComponents() {
return viewModelComponents;
}
public static class ViewModelComponentsHolder {
private final EventRegisterComponent eventRegisterComponent;
private EventRegisterTopComponent eventRegisterTopComponent;
private PlaceSelectComponent placeSelectComponent;
private ViewModelComponentsHolder(EventRegisterComponent eventRegisterComponent) {
this.eventRegisterComponent = eventRegisterComponent;
}
// Top view
public EventRegisterTopComponent eventRegisterTopComponent() {
if (eventRegisterTopComponent == null) {
eventRegisterTopComponent = DaggerEventRegisterTopComponent.builder()
.eventRegisterComponent(eventRegisterComponent)
.build();
}
return eventRegisterTopComponent;
}
public void releaseEventRegisterTopComponent() {
eventRegisterTopComponent = null;
}
// Place view (上記同様、get/release を用意するので以下略)
// (略)
}
}
-
EventRegisterGraph
が生きている限り、EventRegisterComponent
は唯一のインスタンスです (つまりEventRegisterGraph
=@EventRegisterScope
で生存する) -
EventRegisterGraph
の生存期間内で、@ViewModelScope
のインスタンスは生成/開放が起きるので、それらに関しては getter と release するメソッドが居ます -
@ViewModelScope
の component が何度作られようともEventRegisterComponent
のインスタンスは同じなので、@ViewModelScope
からアクセスする@EventRegisterScope
内のインスタンス (model) は同じになります- これで全ての画面が同じ model へアクセスする事が容易になりました
ライフサイクルを実現する
-
EventRegisterGraph
インスタンスのライフサイクルを投稿開始から完了まで (@EventRegisterScope)
にするにはもうひと考慮必要で、問題は 3 つあります- いつ生成/解放するか
- 誰が生成/解放するか
- 誰に保持させるのか
-
今回は以下のように考えました
- 投稿開始時に起動される
Activity
の、初回のonCreate()
(saveInstanceState
がnull
)で生成してfinish()
で解放 - 上記の
Activity
-
Application
クラス
- 投稿開始時に起動される
-
理由は下記の通りです
1. ユーザにとっての投稿開始と終了、はこれらのタイミングと一致するから
2. これらのタイミングをフックできるのは `Activity` クラスのみ
3. Android framework 上で `Activity` より長いライフサイクルを持つのは `Application` クラスしか無いし、
static 変数を用意して global で管理するよりは筋がよいと判断
(`Application` クラスにアクセスするには `Context` が必要)
- 正直これがベストかどうかは分かりませんし、若干スマートじゃないなぁという気もしています
- ただ、ライフサイクルの管理はユースケースや実装する機能で方法も大きく変わる所だと思うので、大外れでなければそれで良いのではと感じました
それらをinjectionしていく
- それでは、依存グラフを用いて UI を含めた全体像を固めます
- 今回は下図のような全体像に落ち着きました
簡易版の実装を用意しました
- 依存グラフを組み立ててから UI レイヤを組んでいくアプローチで、シンプルで小さいアプリを組みました
- この post で述べたポイントのみを実装した (何の機能も持ってない) アプリですので、より具体的にポイントを感じて頂けるかと思います
- https://github.com/tsuyoyo/scope-driven-layered-architecture
まとめ
- データの引き回しが何気に面倒な "投稿フォーム画面" の実装を例に、インスタンスのScope を熟考したアーキテクチャの組み方を書いてみました
- Dagger2 を活用して 依存グラフを組む方法 や、Scope を Android 上で実現する方法 をいくつか提案しました
- Android アプリを組む考え方の一つとして、誰かの参考になれば幸いです m(_ _)m