108
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Dagger2のscopeの使い方を正しく理解する

Last updated at Posted at 2017-03-29

この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クラスに保持させる事でそれが実現できます
  • 下記のコードはまさに ApplicationComponent を管理させてますね
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するインスタンスの生存期間を指定します
    • よって下記のMyViewModelMyComponentと同じ生存期間を持つことになります
    • 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) になっている点に注意して下さい

@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つしかないように見える
    • 本当?
    • もしかして手段があったりするんじゃないの?
108
67
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
108
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?