6
5

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 3 years have passed since last update.

【DI】Dagger2+Retrofit2(+OkHttp3)+ViewModelのDIの最小構成[その2]

Last updated at Posted at 2019-12-25

はじめに

この記事は【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に指定してます。

Api.kt
interface Api
ApiModule.kt

@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 していたため、該当コードを削除しました。

AppComponent.kt

@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 さんのこちらの記事を参考にさせて頂きました。

ViewModelFactory.kt
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)
        }
    }
}
ViewModelKey.kt
@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を実装します。

MainViewModel
class MainViewModel @Inject constructor(private val useCase: MainUseCase): ViewModel()

MainViewModelからUseCaseを呼び出したいため、コンストラクタでMainUseCaseをInjectしています。
ちなみにMainUseCaseではMainRepositoryをInjectし、MainRepositoryではApiをInjectする想定です。

MainUseCase
class MainUseCase @Inject constructor(private val repository: MainRepository) 
MainRepository
class MainRepository @Inject constructor(private val api: Api)

ViewModelをBindします。
MainFragmentと同じScopeにするため、MainFragmentModuleに追加しています。

MainFragmentModule.kt
@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を指定してあげます。

MainFragment
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したクラスに記載する事が多いです。

MainUseCase※MainRepositoryも同様
interface MainUseCase {
    fun hoge()
}

class MainUseCaseImpl @Inject constructor(private val repository: MainRepository): MainUseCase {
    override fun hoge() {
         〜実際の処理〜
    }
}

その場合、下記のようにDIを行います。

ここがいつも悩んでいるところで、AppComponentにModuleを記載するのが良いのか、MainActivityBuilderにModuleを追加する形の方が良いのかよく分かってないのです。
Daggerに詳しい方いましたらご意見ください。

MainModule.kt
@Module
internal object MainModule {
    @Singleton
    @Provides
    @JvmStatic
    fun provideMainRepository(api: Api): MainRepository =
        MainRepositoryImpl(api)

    @Singleton
    @Provides
    @JvmStatic
    fun provideMainUseCase(repository: MainRepository): MainUseCase =
        MainUseCaseImpl(repository)
}
AppComponent.kt
@Singleton
@Component(
    modules = [
        AndroidInjectionModule::class,
        AppModule::class,
        ApiModule::class,
        MainActivityBuilder::class,
        MainModule::class // 追加
    ]
)

余談2 ViewModelのインスタンス化について

上記記事ではlateinit varを使いましたが、最近はAndroid-KTXを使うことによって、下記のように書けるようになったようです。

groovy@build.gradle
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc04' // 追加
private val viewModel: MainViewModel by viewModels()

ViewModelFactoryを使う場合は下記のように書けます。

MainFragment
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 の取得に便利だ

6
5
2

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
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?