Dagger 勉強日記(第2話) - CodeLab実況中継
ここからはCodeLab(↓)に従って、演習をやってみながら書いた私の実況中継的なメモ・まとめとなっています。
CodeLabをやりながら補完的にこちらの記事を参考にしていただくと、演習をやる上で集中して覚える・考えるポイントはどこなのかがつかめると思います。
第5章 Constructor Injection
@Injectをコンストラクタに加える
ここで、もともとのコンストラクタは下の様になっているのに、
class RegistrationViewModel(val userManager: UserManager) {
なぜ、@Injectを加えたあとは、下のように、
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
@Injectの後に、constructorという文字が来ているのか?
というのがまず疑問に思ったところでしたが、すぐ下に以下のように書いてありました。
つまり、Kotlinでは、コンストラクタにアノテーションをつけるときは、明示的にconstructorというキーワードをつける必要があるということでした。
今まで、アノテーションプロセッサを使うライブラリはたくさん使って来ましたが、恥ずかしながら知りませんでした。
(おそらく@Injectアノテーションを加えたことで、コードの構造をアノテーションプロセッサが把握する時にどこがコンストラクタなのか混乱するので、constructorというキーワードを追加して明示的にコンストラクタであることをアノテーションプロセッサに教えて上げる必要があるのかなと思いました。)
@Injectの目的はDaggerが
そのクラスをどう作ればよいかを知るため
そのクラスを作る時に挿入される依存関係がなにかを知るため
の2つのようです。何やらすごく本質的なことを言っているようなので、深く心に留めて置こうと思いました。
こうあるので、Interfaceには、また@Injectとは違う方法で依存関係を構築する必要がありそうです。
Field Injection
ActivityやFragmentは「コンストラクタを使って初期化する」という使い方はしないので、今までセッターや、onCreateの中で他のインスタンスを自分で初期化して依存関係を作っていました。
これをDaggerを使って自動化するというステップが次にでてきます。
まずいちいち、各依存関係の変数の宣言の上に@Injectを足すようです。
同時にこのコードラボのRegistrationActivityの例では、その依存関係を初期化している部分をonCreate()の中から取り除きます。
こんなので、依存関係が注入されるのか不安になりましたが、次の章で説明があるようです↓
How can we tell Dagger which objects need to be injected into RegistrationActivity? We need to create the Dagger graph (or application graph) and use it to inject objects into the Activity.
どうやら、Dagger graphというものを作るようです。
第6章 アプリケーショングラフ(DaggerGraph)
ComponentアノテーションをつけたAppComponentというインターフェースを作成します。
どうやらDaggerはこのAppComponentを元にしてアプリケーショングラフ(Dagger Graph)というものをつくるようです。
このAppComponentの中で
fun inject(activity: RegistrationActivity)
のようなメソッドを宣言するように指示があります。
このメソッドをつかって、RegistrationActivityのなかの@Injectをつけた依存関係をDaggerに用意させる、
と、ひとまず理解しました。
(このメソッドが呼ばれる部分は後のステップででてきました)
Daggerは、このときに、注入するオブジェクトの更に依存関係を見に行くので、その先に必要なオブジェクトの依存関係が見つからないとエラーがでるようです。
このサンプルだと、RegistrationActivityに提供するRegistrationViewModelオブジェクトを作成する際にUserManagerとの依存関係が挿入されますが、UserMangerのコンストラクタには、Storageインターフェースのオブジェクトが渡されるようになっています。
このStorageオブジェクトをまだDaggerが理解してないので、まだエラーがでます。
つまり、
RegistrationActivity -> RegistrationViewModel ○ -> UserManager ○ -> Storage ✗
というのがここまでの状態です。
7章でここを解決してくれるようです。
第7章 アプリケーショングラフにいろいろ追加する
Storageはインターフェースなので、Daggerは、UserManagerのコンストラクタを@Injectで理解しただけでは、Storageオブジェクトの提供の仕方(初期化の仕方)がわかっていません。
これを解決する方法が7章にでてきますが、ここからかなり、ぐぐぐぐ、、という展開になっていきます。
ここで@ModuleというアノテーションをつけたAbstract Class、StorageModuleを作らないといけないのですが、この章はかなり頭にストレスが溜まってくる(陸上に例えるなら体中に乳酸が溜まって来る距離に差し掛かったような)箇所ですが、
この章の理解を助けるのに良いものは、各コードスニペットにコメントが足されていることです。たとえば、StorageModule.ktには下のようなコメントがあります。
// Tells Dagger this is a Dagger module
// Because of @Binds, StorageModule needs to be an abstract class
このコメントが的確で、説明文以上に今時分のやっている作業が何なのかを示唆してくれるので、私はこのコメントもコピーして手元の自分のプロジェクトに加えていきました。これで自分を見失いそうになるのをなんとか堪えることができました。
この章あたりで
依存関係の解決に必要なもの(つまりあるクラス(この例ではRegistrationActivity)の初期化に必要なもの)は、全部Application Graph (Dagger Graph)に用意されている状態をつくる、
というのが肝だということがだんだんわかってきました。
Application Graph(つまりAppComponent)にすべての依存関係を引き寄せる。その際に
InterfaceはModuleをつくって、それををincludeする
@Component(modules = [StorageModule::class])
Contextみたいな天から降ってくるもの(アプリの起動時にはもう初期化されているもの)は、Factoryを書いて、取り込む
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): AppComponent
}
このようにしてApplication Graphにすべてを引き寄せた上で、Activityクラスに依存関係をインジェクトする
fun inject(activity: RegistrationActivity)
というような流れが見えてきました。
第8章 AppComponentの初期化
// Instance of the AppComponent that will be used by all the Activities in the project
val appComponent: AppComponent by lazy {
// Creates an instance of AppComponent using its Factory constructor
// We pass the applicationContext that will be used as Context in the graph
DaggerAppComponent.factory().create(applicationContext)
}
AppComponentインターフェースのオブジェクトをMyApplicationクラスで初期化しています。また、このとき、前のステップで実装したFactoryのcreateメソッドを呼び出していて、納得が行きました。引数にはApplication Contextのインスタンスを渡しています。
(AppComponentとしてつくったインターフェースは今や自動的にDaggerによって、DaggerAppComponentというクラスに実装されているので、このクラスのfactory()メソッドを読んでオブジェクトを作っています。)
次に、RegistrationActivityのonCreateのなかで、先程AppComponentに追加したinjectメソッドを呼び出すところがとうとう出てきました。
// Ask Dagger to inject our dependencies
(application as MyApplication).appComponent.inject(this)
これでAppComponentに書いたメソッドはすべてどうやって使われるのかがはっきりしました。
注意点として、inject()は、super.onCreate()の前に呼び出す必要があるようです。
もう1つこの章で出てきた注意ポイントとしては、@Injectをつけた変数は、privateではないけないので、privateを取る必要があるということです。
// @Inject annotated fields will be provided by Dagger
@Inject
private lateinit var userManager: UserManager
@Inject
private lateinit var mainViewModel: MainViewModel
こういうのはダメで、privateをとらないといけないようです。
第9章 シングルトン
シングルトンな依存関係を定義する方法が書いてあります。
AppComponent(アプリケーショングラフ)のなかで、UserManagerがシングルトンなオブジェクト(依存関係)として存在するようにできます。
第10章 スコープとサブコンポーネント
@Singleton(アプリの起動中ずっと共有されてほしいシングルトンのオブジェクト)ではなく、あるActivityのライフサイクルだけ1つのインスタンスが共有される(複製されない)ような依存関係を注入したい、というケースがこの章ででてきます。
これは、AppComponentの下にSubcomponentをつくることによって実現します。
Subcomponentについては以下の記述が重要だと思いました。
つまり、Subcomponentからは親のAppComponentのオブジェクト全てにアクセスできます。
この章でさり気なくでてきたことで、絶対にハマりポイントになると感じたのは、、
Fragmentでは、injectはOnCreateViewではなく、onAttachでやることと、super.onAttach()の後にやること
です。
class EnterDetailsFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
(requireActivity().application as MyApplication).appComponent.inject(this)
}
}
第11章
↓がスコープのルールになります。原理原則的な匂いがしたのでしっかりよみました。
Scoping rules:
When a type is marked with a scope annotation, it can only be used by Components that are annotated with the same scope.
When a Component is marked with a scope annotation, it can only provide types with that annotation or types that have no annotation.
A subcomponent cannot use a scope annotation used by one of its parent Components.
Components also involve subcomponents in this context.
ここまできて、同じスコープ名を、複数の異なるSubcomponentsで使っていいのかな、というのが疑問に浮かびました。サンプルコードでは@ActivityScopeといういかにも使いまわしの聞く名前でScopeを定義しているので、良さそうに見えす。
(その後の章をやってみて、やはり、同じ名前のスコープをことなるSubcomponentで使いまわして良いことがわかりました。なので、スコープの名前は範囲・用途を元につけるのが良さそうです。)
さて、この章でつくったサブコンポーネントであるRegistrationComponentの寿命はどうきまるのか、というこの章で一番重要そうなトピックに入ってきました。
registrationComponentは、@Inject付きで定義されていません。
// Stores an instance of RegistrationComponent so that its Fragments can access it
lateinit var registrationComponent: RegistrationComponent
中段以降にこう書かれていました。
つまり、RegistrationActivityのなかでは、registrationComponentを初期化する(依存関係を挿入する)のはDaggerではないから、自分でやれということのようです。
思い返してみると、MyApplicationでもappComponentには@Injectアノテーションはついておらず、自分で初期化するコードを書いていました↓
// Instance of the AppComponent that will be used by all the Activities in the project
val appComponent: AppComponent by lazy {
// Creates an instance of AppComponent using its Factory constructor
// We pass the applicationContext that will be used as Context in the graph
DaggerAppComponent.factory().create(applicationContext)
}
このことから、
Componentは親、サブ関わらず自分で初期化する(依存関係を挿入する)
ということがわかります。少し言い換えると
親ComponentはApplicationクラスで、サブComponentはそのサブComponentのスコープに対応したActivityで手動で初期化する
ということがわかりました。
第12章は、Loginのフローに今までやったことを適用するだけなので、いいおさらいができました。
第13章
先程まではSubcomponentを作って対応するActivityの依存関係を注入するという演習でしたが、この章で出てくるSettingActivityは、Subcomponentではなく、親のAppComponentでInjectします。これはSettingActivityがアプリ自体の寿命と一蓮托生だからこういう設計になっているのだろうと理解しました。
次に、UserDataRepositoryは@Singletonでいいのかな?と思いましたが、
↓が書いてありました。
Can we scope UserDataRepository to AppComponent by annotating it with @Singleton? Following the same reasoning as before, we don't want to do it because if the user logs out or unregisters, we don't want to keep the same instance of UserDataRepository in memory. That data is specific to a logged in user.
つまり、UserDataRepositoryはアプリのライフサイクルのなかで、ユーガーがログアウトするときに一旦クリアされる存在だから、Singletonにしないことが望ましい、ということでした。
そこで、この章では、最終的には間接的にUserDataRepositoryのデータを司るUserComponentというコンポーネントを作ります。
UserComponentは今までの例と違い1つのActivityと一蓮托生じゃなく、複数のActivityから使われるコンポーネントになります↓
What is in charge of the lifetime of UserComponent? LoginComponent and RegistrationComponent are managed by its Activities but UserComponent can inject more than one Activity and the number of Activities could potentially increase.
では、UserComponentは何と一蓮托生にすればいいのか、↓に書いてありました。
We have to attach the lifetime of this Component to something that knows when the user logs in and out. In our case that's UserManager.
UserManagerがLogin -> Logoutまでのタイミングを知っているので、ここにUserComponentを結びつけよう、ということのようです。
ここで、UserManagerのコンストラクタ引数として、UserComponentのFactoryを渡す、という今までにない形がでてきます。
今までは、AppComponentにSubcomponentのFactoryを渡すためのメソッドを追加して、これを各ActivityがonCreateの中でつかっていました。これとは今回違うパターンの登場です。
userManagerをMyApplicationで削除し代わりにAppComponentのなかで以下のようなメソッドを宣言して、AppComponent(アプリケーショングラフ)の中に注入された状態にします。
// 2) Expose UserManager so that MainActivity and SettingsActivity
// can access a particular instance of UserComponent
fun userManager(): UserManager
16章 ミニ演習
今のままだと、MainActivityのなかでログインの有無をみて、ログインしているときだけ依存関係の注入を行っています。このConditional Dependecy Injectionというのが良くないようです。
そこで、LoginとRegistrationのフローを受け取るSplashActivityを受け取るようにして、そのなかでログイン状態を確認し、ログインしていればMainActivityに遷移するようにしなさい、
という問題でした。
SplashActivityで必要な依存関係は、SplashActivity生存期間中しか必要ないので、LoginActivityやRegistrationActivityの時にやったようにActivity専用のSubcomponentを作成するパターンでいけました。スコープもLoginComponentやRegistrationComponentを作ったときと同じ@ActivityScopeが使えました。
(注意点として、AndroidManifestにSplashActivityを登録する際、SplashActivityをLaunch Activityに登録しておくことが必要です。)
このパターンは自分でも大丈夫だなあという自身がありました。
第13章のUserComponentをつかったパターンが自分のなかでまだ頭で完全に理解できていない感覚があります。UserManagerの依存を注入し、そこで、UserComponentを紐付けるというのが、まだふに落ちなかったので、もう一度、この部分を、アプリ起動の一番はじめとなるMyApplicationのonCreateから追って見ようと思います。
このように、
AppComponentの中に注入された依存関係であるuserManagerのなかに、Subcomponentをもたせ、それを条件(ログイン済みという条件)によってcreateし、その後、このSubcomponentの有無によって、遷移先のActivityを変える、
というのがこのパターンのようです。
Subcomponentがダイナミックにcreateされること、と、Subcomponentの格納先がActivityではない、という点が理解できるまでに時間がかかりました。
感想
自分には珍しいくらい集中してDaggerのコードラボをやった感想としては、「やっぱり難しい!」です。特にライフサイクルとからめて必要な依存関係(だけ)を注入することをイメージできるようになるのにまだ時間が必要だなと思いました。
また、初習者には向いてないな、というのも感想です。
初習者、とくにプログラミング自体の初習者には今までのように自分で手作業で依存関係を受け渡していくプログラミングをしたほうが、アプリのライフサイクルのなかで画面(View)とデータ(モデル)がどのように連動してアプリを提供しているのかを実感でき、学べることが多いと思います。
あくまで個人的な感想ですが、まずは、自分で依存関係をしっかり管理してアプリが作れるようになってからDaggerを使ったほうがよいと思いました。
もし開発チームがエキスパートだけのチームであれば、Daggerを使う前提にしておいて、アプリケーショングラフをみんなで最初に書きあったりすると、アーキテクチャのアイデアの共有になって良さそうだと思いました。
依存関係に絡んだバグを作り込まないようにする、という目的のためよりは、設計段階でアプリの依存関係を話し合える、開発の上流で依存関係をある程度決める、という開発スタイルをつくれることがDaggerを使う良さなのかも、と感じました。
この記事が気に入ったらサポートをしてみませんか?