はじめに
この記事は【DI】Dagger2+Retrofit2(+OkHttp3)+ViewModelのDIの最小構成[その1]の続きです。
今回はRetrofitとViewModelをDIしていきます。
2019/12/26 追記
どうやら最新バージョンのDagger(2.25.3)で本記事のやり方でDIしようとすると、複数のViewModelを生成するとエラーとなってしまうようです。
2.24では問題ないです。最新版での良いやり方を見つけたら本記事に追記します。
2019/12/27 追記
2.25.3でも問題なかったです。変なところにNamed
を付けてエラーが出てました。
ただ、@Component.Builder
を使ったやり方は若干古かったようなので、[その1]と併せて記述を修正しました。
4. Retrofitの依存性注入
ApiクラスとApiModuleを追加します。
ベースURLはGitHubのAPIに指定してます。
interface Api
@Module
class ApiModule {
companion object {
const val API_READ_TIMEOUT: Long = 10
const val API_CONNECT_TIMEOUT: Long = 10
}
@Provides
@Singleton
fun provideOkhttpClient(): OkHttpClient {
val logInterceptor = HttpLoggingInterceptor()
logInterceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor {
val httpUrl = it.request().url
val requestBuilder = it.request().newBuilder().url(httpUrl)
it.proceed(requestBuilder.build())
}
.addInterceptor(logInterceptor)
.readTimeout(API_READ_TIMEOUT, TimeUnit.SECONDS)
.connectTimeout(API_CONNECT_TIMEOUT, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.github.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
@Provides
@Singleton
fun provideAPI(retrofit: Retrofit): Api {
return retrofit.create(Api::class.java)
}
}
JSONのパース用にgsonを使っています。
今回の主題ではないので詳細は省略します。
2020/01/07追記
JakeWharton氏曰く「Gson is deprecated.」との事だったので、GsonからMoshiに変更しました。
AppComponentにApiModuleの依存性を追加します。
2020/03/29追記
ApiModuleを不要に BindsInstance
していたため、該当コードを削除しました。
@Singleton
@Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
MainActivityBuilder::class,
ApiModule::class // 追加
]
)
interface AppComponent : AndroidInjector<App> {
@Component.Factory
interface Factory {
fun create(@BindsInstance app: App): AppComponent
}
}
5. ViewModel(ViewModelFactory)の依存性を注入する
引数付きのViewModelを扱うために、ViewModelFactory
を作成します。
@satorufujiwara さんのこちらの記事を参考にさせて頂きました。
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T: ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<out ViewModel>? = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
break
}
}
}
if (creator == null) {
throw IllegalArgumentException("unknown model class " + modelClass)
}
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
続けてViewModelを作成します。
今回はMainFragmentから呼び出すMainViewModelを実装します。
class MainViewModel @Inject constructor(private val useCase: MainUseCase): ViewModel()
MainViewModelからUseCaseを呼び出したいため、コンストラクタでMainUseCaseをInjectしています。
ちなみにMainUseCaseではMainRepositoryをInjectし、MainRepositoryではApiをInjectする想定です。
class MainUseCase @Inject constructor(private val repository: MainRepository)
class MainRepository @Inject constructor(private val api: Api)
ViewModelをBindします。
MainFragmentと同じScopeにするため、MainFragmentModuleに追加しています。
@Module
internal abstract class MainFragmentModule {
@ContributesAndroidInjector
@FragmentScope
abstract fun provideMainFragment(): MainFragment
// 追加
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
@FragmentScope
internal abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
}
最後です。
MainViewModelでViewModelのインスタンス取得処理を書きます。
この時ViewModelFactoryを指定してあげます。
class MainFragment : DaggerFragment() {
@Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
viewModel = ViewModelProvider(this, viewModelFactory).get(MainViewModel::class.java)
return inflater.inflate(R.layout.fragment_main, container, false)
}
これでひとまず完成です。
正常にビルドが通っていればDIが成功しています。
余談1 UseCase、Repositoryをinterface化する
上記の記事ではUseCase、Repositoryを直接DIしましたが、
私の場合、UseCaseやRepositoryはinterfaceとして実装し、実際の処理はimplementsしたクラスに記載する事が多いです。
interface MainUseCase {
fun hoge()
}
class MainUseCaseImpl @Inject constructor(private val repository: MainRepository): MainUseCase {
override fun hoge() {
〜実際の処理〜
}
}
その場合、下記のようにDIを行います。
ここがいつも悩んでいるところで、AppComponentにModuleを記載するのが良いのか、MainActivityBuilderにModuleを追加する形の方が良いのかよく分かってないのです。
Daggerに詳しい方いましたらご意見ください。
@Module
internal object MainModule {
@Singleton
@Provides
@JvmStatic
fun provideMainRepository(api: Api): MainRepository =
MainRepositoryImpl(api)
@Singleton
@Provides
@JvmStatic
fun provideMainUseCase(repository: MainRepository): MainUseCase =
MainUseCaseImpl(repository)
}
@Singleton
@Component(
modules = [
AndroidInjectionModule::class,
AppModule::class,
ApiModule::class,
MainActivityBuilder::class,
MainModule::class // 追加
]
)
余談2 ViewModelのインスタンス化について
上記記事ではlateinit var
を使いましたが、最近はAndroid-KTXを使うことによって、下記のように書けるようになったようです。
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc04' // 追加
private val viewModel: MainViewModel by viewModels()
ViewModelFactoryを使う場合は下記のように書けます。
private val viewModel: MainViewModel by viewModels { viewModelFactory }
valになり、1行で書けるようになったのでスッキリしましたね。
最後に
記事を書くにあたって以下を参考にさせて頂きました。
Daggerは本当に難しい。。。
・Dagger2 + Retrofit2 + Moshi + Kotlin を使って通信するまで
・Dagger2: 2.23に入ったHasAndroidInjectorについて
・Architecture Components を Dagger2 と併用する際の ViewModelProvider.Factory について
・Fragment の Android-KTX が AAC ViewModel の取得に便利だ