在宅勤務になり時間ができてきたので、今まで難しくて手を出すのを躊躇っていた Dagger に挑戦しようと思いました。
Room のまとめをやろうと思っていましたが、自分の中で HOT な物からと思って順番が前後してます。
メッセージをリストで表示するチャットアプリ ライク なプロジェクトを MultiModule にしました。
これを Dagger を使ってモデル層を DI するお勉強です。
色んな記事を見ましたがひとまずとっかかりを作るため、先人の知恵をフンダンに使用しています。理解するぞ
ほぼほぼ覚書なので雰囲気を掴んでいただけたら幸いです。。。
Environment
- Android Studio: 3.6
- Gradle Plugin: 3.5.2
Dagger のライブラリは以下
(MultiModule 前提なので buildSrc に纏めています。)
今回は Android 用のサブライブラリを用いて実装していきます。
object Dagger {
const val version = "2.26"
const val core = "com.google.dagger:dagger:$version"
const val compiler = "com.google.dagger:dagger-compiler:$version"
const val android = "com.google.dagger:dagger-android:$version"
const val androidSupport = "com.google.dagger:dagger-android-support:$version"
const val androidProcessor = "com.google.dagger:dagger-android-processor:$version"
object AssistedInject {
const val version = "0.5.2"
const val annotations = "com.squareup.inject:assisted-inject-annotations-dagger2:$version"
const val processor = "com.squareup.inject:assisted-inject-processor-dagger2:$version"
}
}
もともとのソースコードはこんな感じです。
(ソースコードから抜粋しているので色々抜けてるかも。。。雰囲気を掴んでもらえれば。。。)
app/src/main/java/
├── MainActivity.kt
└── MyApplication.kt
feature/message/src/main/java/
└── message/
│ ├── MessageFragment.kt
├── item/
│ ├── MessageHeaderItem.kt
│ └── MessageItem.kt
└── viewmodel/
└── MessageViewModel.kt
domain/repository/src/main/java/
└── repository/
└── MessageRepository.kt
└── internal/
└── MessageRepositoryImpl.kt
domain/db/src/main/java/
└── db/
├── MessageDataBase.kt
├── dao/
│ └── MessageDao.kt
└── entity/
└── MessageEntity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val binding: ActivityMainBinding by dataBinding()
private val navController: NavController by lazy {
findNavController(R.id.fragment_container)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupToolbar()
}
private fun setupToolbar() {
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController)
}
}
class MessageFragment: Fragment(R.layout.fragment_message) {
private val binding: FragmentMessageBinding by dataBinding()
private val viewModel: MessageViewModel by viewModelProvider {
MessageViewModel("00001", MessageRepositoryImpl(MessageDatabase.getIntance(context)))
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupMessageList()
}
private fun setupMessageList() {
val messageListAdapter: GroupAdapter<GroupieViewHolder<*>>()
binding.messageList.adapter = messageListAdapter
viewModel.messageList.observe(viewLifecycleOwner) {
val items = it.map { entity -> MessageItem(entity) }
messageListAdapter.update(items)
}
}
}
class MessageViewModel(private val roomId: String,
private val repository: MessageRepository): ViewModel() {
val messageList: LiveData<List<MessageEntity>> by lazy {
repository.messageList(roomId).asLiveData()
}
}
interface MessageRepository {
fun messageList(roomId: String): Flow<List<MessageEntity>>
}
class MessageRepositoryImpl(private val db: MessageDataBase): MessageRepository {
override fun messageList(roomId: String): Flow< List<MessageEntity>> =
db.messageDao().observe(roomId).distinctUntilChanged()
}
abstract class MessageDatabase: RoomDatabase() {
...
companion object {
private const val DATABASE_FILE = "message.db"
fun getInstance(context: Context): MessageDatabase =
Room.databaseBuilder(context, MessageDatabase::class.java, DATABASE_FILE).build()
}
}
また、ViewModel に引数を渡す時に使う拡張関数を定義しています。
inline fun <reified T: ViewModel> Fragment.viewModelProvider(crossinline viewModels: () -> T): Lazy<T> {
return viewModels {
object: ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return viewModels() as T
}
}
}
}
Fragment の ViewModel に Repository を Inject するまでを目標にがんばります。
DI
Repository を Activity -> Fragment -> ViewModel の順番に DI していきます。
準備
Module
- DI するインスタンスを定義するところ
今回は Repository を実装したクラスのインスタンスを用意します。
@Module
class AppModule {
@Singleton @Provides
fun provideRepository(app: Application): MessageRepository {
val db = MessageDatabase.getInstance(app)
return MessageRepositoryImpl(db)
}
}
Component
- DI するインスタンスを定義するクラスを指定するところ
上記で作成した Module を指定します。
今後 Module を増やした場合には、ここに増やしていく感じですね。
@Singleton
@Component(modules = [AndroidInjectionModule::class, AppModule::class])
interface AppComponent: AndroidInjector<MyApplication> {
@Component.Factory
interface Factory {
fun create(@BindsInstance app: Application): AppComponent
}
}
ポイントは
- サブライブラリを用いているため modules に AndroidInjectionModule の指定
- Module の provide の引数と Factory の create の型を合わせる事で、BindsInstance のおかげで Dagger がうまいこと解決してくれる
です。
最後に、作成した Component を init します。
基本的に Singleton はアプリの生存期間と一致させたいと思うので Application 内に定義します。
class MyApplication: DaggerApplication() {
private val appComponent: AppComponent by lazy {
DaggerAppComponent.factory().create(this)
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> = appComponent
}
本来は HasAndroidInjector
interface を実装する必要があり、onCreate で inject をしてあげる必要があります。
(やり方は次の Activity への Inject を参照)
が、 Dagger がその辺の Base クラスを提供してくれているため injector (今回は Component) を返すだけでよしなにしてくれます。
Activity に Inject してみる
では作成した Component を使って実際に Activity に Inject してみます。
まずは MainActivity に HasAndroidInjector
を実装します。
Activity にも Dagger が提供している便利 Base クラスが存在するのですが、
dataBinding の拡張関数でコンストラクタに渡したレイアウトファイルから Bind しているため、コンストラクタに引数を渡せないので本来の実装をしています。
class MainActivity: AppCompatActivity(R.layout.activity_main), HasAndroidInjector {
@Inject lateinit var injector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> = injector
@Inject lateinit var db: MessageRepository
override fun onCreate() {
AndroidInjection.inject(activity)
}
}
ここで AndroidInjection を使うため (DispatchingAndroidInjector が依存解決できる様)に、SubComponent とそれに対する Module を定義し、Injector を使用するクラスを Bind させないといけません。
が、ボイラープレートなコードがたくさんできあがるため ContributesAndroidInjector
を使う事で自動で SubComponent を作ってくれ Bind してくれるのでスッキリ書く事ができます。
@Module
abstract class MainActivityModule {
@ContributesAndroidInjector
abstract fun contributeMainActivity(): MainActivity
}
Module (SubComponent) を追加したらメインの Component に定義します。
@Component(modules = [AndroidInjectionModule::class, AppModule::class, MainActivityModule::class])
interface AppComponent: AndroidInjector<MyApplication> {
@Component.Factory
interface Factory {
fun create(@BindsInstance app: Application): AppComponent
}
}
これで MainActivity に Repository が Inject されました。
Fragment に Inject してみる
次は Repository を Fragment に Inject してみます。
class MessageFragment: Fragment(R.layout.fragment_message) {
...
@Inject lateinit var repository: MessageRepository
private val viewModel: MessageViewModel by viewModelProvider {
MessageViewModel("00001", repository)
}
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
}
...
}
Activity との違いは
- HasAndroidInjector を実装しない
- onAttach で inject
- SupportLibrary の Fragment を使っている場合は AndroidSupportInjection を使う
です。
後は Activity と同じく Injection を使うために Module を定義します。
@Module
abstract class MainActivityModule {
// modules に指定する事で SubComponent として扱われる様になる
@ContributesAndroidInjector(modules = [MainActivityBinder::class])
abstract fun contributeMainActivity(): MainActivity
@module
abstract class MainActivityBinder {
@ContributesAndroidInjector
abstract fun contributeMessageFragment(): MessageFragment
}
}
MainActivityModule の SubComponent として定義しているため依存関係が引き継がれるので AppComponent に Module を定義する必要はありません。
これで Fragment に Repository が inject されました。
。
。。
。。。
さて、この記事は最初に1つウソをついています😂
最初 Dagger を導入してやってみたときはこんなにスルッといきませんでした😂
と言うのも、今回 Groupie を使っており元々の Fragment のソースコードは Adapter をメンバーに持っておりました。
class MessageFragment: Fragment(R.layout.fragment_message) {
private val binding: FragmentMessageBinding by dataBinding()
@Inject lateinit var repository: MessageRepository
private val viewModel: MessageViewModel by viewModelProvider {
MessageViewModel("00001", repository)
}
private val messageListAdapter: GroupAdapter<GroupieViewHolder<*>> by lazy {
GroupAdapter<GroupieViewHolder<*>>()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupMessageList()
}
private fun setupMessageList() {
binding.messageList.adapter = messageListAdapter
viewModel.messageList.observe(viewLifecycleOwner) {
val items = it.map { entity -> MessageItem(entity) }
messageListAdapter.update(items)
}
}
...
}
この時にビルドすると
- GroupAdapter が見つかりません
-
[ComponentProcessor:MiscError] dagger.internal.codegen.ComponentProcessor was unable to process this interface because not all of its dependencies could be resolved.
と、コンパイルエラーでどうにも上手く行きませんでした。
見つけた解決策は例みたいに GroupAdapter をメンバーに持たない様にすると上手くいきましたが原因が分からずモヤモヤします。メンバーにワイルドカードがいるとだめなのか?🤔
どなたかご存知でしたらご教授ください。。。。
Repository が Inject された ViewModel を Fragment に Inject する
圧倒的ルー大柴感。
まずは Repository を ViewModel に Inject します。
この時、例えばルームの ID に紐付いたメッセージの一覧を表示したい場合はコンストラクタに ID を渡したりしますよね。
そういった Inject とは別に引数を渡したい場合に AssistedInject
を使います。
GitHub - square/AssistedInject: Assisted injection for JSR 330.
@Inject の代わりに @AssistedInject
を使い、外から渡したい引数に @Assisted
をつけます。
そして AssistedInject 用の ViewModel の Factory を作成します。
class MessageViewModel @AssistedInject constructor(@Assisted private val roomId: String,
private val repository: MessageRepository): ViewModel() {
val messageList: LiveData<Map<String, List<UserMessageEntity>>> by lazy {
repository.messageList(roomId).asLiveData()
}
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): MessageViewModel
}
}
Factory を Fragment 内に Inject して create を呼ぶ事で Repository が Inject された ViewModel のインスタンスが提供されます。
class MessageFragment: Fragment(R.layout.fragment_message) {
...
@Inject lateinit var viewModelFactory: MessageViewModel.Factory
private val viewModel: MessageViewModel by viewModelProvider {
viewModelFactory.create("00001")
}
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
}
...
}
これで無事 Repository を ViewModel に Inject でき、 Fragment や ViewModel が Repository の実装クラスを意識することがなくなりました。
Conclusion
各 Module ごとに Component を作成し、実装クラスを internal 化したり等はこちらのリポジトリでやっております。
Database のモジュール化に苦戦したりしましたがそれはまた別の機会に。。。
GitHub - tick-taku/NotificationWatcher at feature/project/di_2
やってみた感想は、ただの食わず嫌いで導入するだけなら先人の知恵をなぞればできる()、でした。
(なんとなく)なんかすごい一歩を踏み出せた(気がする)。
(なんとなく)理解した(気がする)。
DroidoKaigi 様様です。。。ありがとうございます。みなさんすごいなぁ(KONAMI)。
ただ、今でもまだ消化しきれていない事も多々あるので間違っているところもあるかもしれません。ご教授いただきたく思います。。。
まだ Dagger が使えると言える程のスキルに達していないので、
ここから自動生成されたソースコードを見たり解説を見たりして何をやっているのかを掴んで理解していこうと思います。
偉大なる先人の方々ありがとうございました。🙇
関係ないですが、なんとなく完全に理解したって言葉好きです。
参考
Using Dagger in Android apps | Android デベロッパー | Android Developers
GitHub - DroidKaigi/conference-app-2020: The Official Conference App for DroidKaigi 2020 Tokyo
Y.A.M の 雑記帳: Android で Dagger を使う(その3 : Android Support)
Dagger Android拡張の使い方 - Kenji Abe - Medium
Dagger2: 2.23に入ったHasAndroidInjectorについて - stsnブログ
DaggerのComponent.BuilderからComponent.Factoryへの移行 - Kenji Abe - Medium