このpostについて
- Dagger2には
@Singletonを始めとした "scope" というものがあります - 下記に1つでも該当する人 (= この前までの自分) は読んでもらえると良いかもです
- Dagger2はActivity lifecycleを克服するために有用な手段だと思ってる
- @Singleton は、いわゆる getInstance() を用いたstaticなシングルトンを実現するためのアノテーションだと思ってる
- Daggerのscopeは、インスタンスのlifecycleをよしなにゴニョゴニョしてくれるmagicであると思ってる
Scopeって何なんだろう
例を交えて考えてみます
-
MyViewModelというクラスのinstanceをinjectionするケースを考えます - "injectする度にinstanceを生成したくない & 一度作ったものを使いまわしたい" という際には下記のようにcomponentとproviderを宣言しますよね
@Singleton
@Component(modules = { MyModule.class })
interface MyComponent {
void inject(MainActivity activity);
MySubComponent createMySubComponent();
}
@Module
class MyModule {
@Provides
MyFakeApi provideMyFakeApi() {
return new MyFakeApi();
}
@Singleton
@Provides
MyViewModel provideMyViewModel(MyFakeApi api) {
return new MyViewModel(api);
}
}
- こうすることで
provideMyViewModelは一度しか呼ばれず、たしかにMyViewModelのインスタンスはシングルトンになります - では "シングルトンな期間" はどこからどこまでなのでしょう?
- これは "Componentの生存期間" です
Componentの生存期間とは
- "
DaggerMyComponent.builder().build()が呼ばれてから、Componentインスタンスから参照が外れるまで" です - よって、例えば "アプリの生存期間内にずっとシングルトンを実現したい" 場合は、Componentを
Applicationクラスに保持させる事でそれが実現できます - 下記のコードはまさに
ApplicationにComponentを管理させてますね
public class MainActivity extends AppCompatActivity {
@Inject
MyViewModel myViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((MyApplication) getApplication()).getMyComponent().inject(this);
}
Scopeの役割
- "Componentの生存期間に名前を付けるもの" と考えると良さそうです
- (多分正確には若干違うのかもしれないけど、解釈としてはズレてないはず)
- 下記の場合は "
MyComponentの生存期間を@Singletonと名付ける" と解釈できます
@Singleton
@Component(modules = { MyModule.class })
interface MyComponent {
void inject(MainActivity activity);
MySubComponent createMySubComponent();
}
@Provider に付随するscopeアノテーション
- provideするインスタンスの生存期間を指定します
- よって下記の
MyViewModelはMyComponentと同じ生存期間を持つことになります -
MyComponentが生きている以上、MyViewModelはシングルトンですね
- よって下記の
@Module
class MyModule {
@Provides
MyFakeApi provideMyFakeApi() {
return new MyFakeApi();
}
@Singleton
@Provides
MyViewModel provideMyViewModel(MyFakeApi api) {
return new MyViewModel(api);
}
}
-
@Singletonという名前が、シングルトンであることと直接結びついていない事が分かります - てことは
@Singletonじゃなくても (scopeを表すアノテーションであれば) 同じ事が起きそうです
CustomScope
- Scopeは、下記のように自分で定義することも出来ます
@Scope
@Retention(RetentionPolicy.RUNTIME)
@interface ViewModelScope {
}
- ここで注意したいのは
@ViewModelScopeと名前をつけたからといって、特に生存期間に対する効力は無いということです - 例えば
Activityのlifecycleと一致させたい!というのであれば、Componentの生存期間をActivityと一致させればよく、Scopeの名前や定義とは一切関係ないという事です - 前述の例で
@Singletonと置いていたところを@ViewModelScopeに置き換えてもなんら動作上変わりません
@Provider に付随させるscopeについて
- さて、先程挙げた例において
@Providerに付随させていた@Singletonを、下記のように@ViewModelScopeへ変更させるとどうなるでしょう
@Singleton
@Component(modules = { MyModule.class })
interface MyComponent {
void inject(MainActivity activity);
MySubComponent createMySubComponent();
}
@Module
class MyModule {
@Provides
MyFakeApi provideMyFakeApi() {
return new MyFakeApi();
}
@ViewModelScope
@Provides
MyViewModel provideMyViewModel(MyFakeApi api) {
return new MyViewModel(api);
}
}
- これはbuild時にerrorが出て (daggerのcompileに失敗する) buildが通りません
- 原因は 「
provideMyViewModelメソッドからすると@ViewModelScopeが、誰の (どのcomponentの) 生存期間なのか分からないため」 です -
MyComponentの@Singletonを@ViewModelScopeにすることで、上記が解決してbuildが通るようになります
こんなこと知ってて何になる?
- SubComponentを使う時に役立ちます (ブラックボックス感が軽減できる、という意味で)
SubComponent
- Dagger2には Component の依存を引き継いで定義ができる "SubComponent" というものがあります
- が、いざ使ってみると謎のトラブルが...、
-
@Singletonをつけると急にbuildがコケる - providerがinjectする度callされる (singletonに期待する事が機能しない)
-
困ったことが起こる原因
- ここ(link)に解答が書いてありました
- "依存しあってるcomponentは同じscopeを共有できない" のだそうです
- よって "Componentに
@Singletonがついていたら、SubComponentに@Singletonを付けるのはNGである" のは、納得ですね
解決例
- "SubComopnentの生存期間内、特定のinstanceをシングルトンにしたい" のであれば、下記のように ComponentとSubComponentでscopeを変えてあげれば良いです
- SubComponentで引っ張っているModuleのproviderのscopeが、SubComponentのscope (
@ViewModelScope) になっている点に注意して下さい
- SubComponentで引っ張っているModuleのproviderのscopeが、SubComponentのscope (
@Singleton
@Component(modules = { MyModule.class })
interface MyComponent {
MySubComponent createMySubComponent();
}
@ViewModelScope
@Subcomponent(modules = { MySubModule.class })
public interface MySubComponent {
void inject(MainActivity activity);
}
@Module
public class MySubModule {
@ViewModelScope
@Provides
MyPojo provideMyPojo() {
return new MyPojo("dance with the wild things");
}
}
じゃあScopeって何のためにあるんだろう
- もちろんDagger2をちゃんと動作させるために必要です
- あとは、Componentの生存期間をチームメンバで共有するためのラベルみたいなものとして考えるのもアリかなと思ってます
- Activityより長くて、Applicationよりも短い生存期間がほしい時もある
まとめ
- Dagger2のScopeの使い方をまとめました
- ブラックボックス感を拭えずに半年くらい使ってきたものの、それが晴れてスッキリしたので、みなさんと共有できればこれ幸いです
余談
Daggerはlifecycle問題をクリアする手段なのか、議論
- 前述の "Componentのlifecycle" で触れましたが、結局Componentが破棄されればprovideされるインスタンスも全て破棄されます
- ApplicationクラスでComponentを管理していれば、Applicationが生存する限りComponentに紐づくものはアプリが生きているうちは全てkeepされます
- ActivityのonCreateでComponentをnewする実装にしていれば、Componentに紐づくもの全てのlifecycleも基本Activityと同じになります
- よって、**Scopeを如何に考慮しても、lifecycle云々の話はまた別の話 (Componentをどうやって管理するのか)**という結論に、自分の中では落ち着いています
気になっていること
- Providerのライフサイクルは、都度createか(Component生存期間内)シングルトンか、の2つしかないように見える
- 本当?
- もしかして手段があったりするんじゃないの?