Kodein(DI)を使ってOkHttp + Retrofit2をより使いやすく (その他便利な使い方紹介)

# はじめに
この記事は [Android Advent Calendar 2018](https://qiita.com/advent-calendar/2018/android) の1日目です！

みなさんはDI使っていますか？とても便利なんですが、外部から依存関係を注入…などパッと理解しづらい説明で難しそうというイメージがあり、利用したことない方も多いと思います。
今回はそんなDIの便利な使い方及び、利用されたことがある方にはOkHttp + Retrofit2の便利な使い方をご紹介したいと思います。

# Kodeinとは？
KOtlin DEpendency INjection、頭文字をとって[Kodein](https://github.com/Kodein-Framework/Kodein-DI)だそうで、DI（依存性注入）ライブラリです。Kotlinに特化しており、Dagger2とは違いKotlinで利用する際にkaptが不要です。
例えば使いまわしたいインスタンスを簡単にシングルトンパターンとして実装することができます。

# Kodeinの最小限の設定方法
`環境: Android Studio 3.2.1, kotlin 1.3.0, kodein 5.3.0`

## gradle
```gradle:app/build.gradle
dependencies {
    def kodein_version = "5.3.0"
    // 略
    implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
    implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
}
```

## Kotlin
- まずFragmentでKodeinを利用する場合の最小限の設定方法を説明します。付随する他のクラスについては[便利な使い方](#便利な使い方)として後述します。

### Module

```kotlin:ExampleServiceModule.kt
// 今回はsingletonバインディングでインスタンスを再利用する
// 他のバインディングタイプが知りたい方は下記リンク参照
// http://kodein.org/Kodein-DI/?5.2/core
fun ExampleServiceModule() = Kodein.Module("ExampleServiceModule") {
    bind<ExampleService>() with singleton { ServiceGenerator.createRetrofit(instance()).create(ExampleService::class.java) }
}
```

### Application

```kotlin:ExampleAndroidApplication.kt
class ExampleAndroidApplication : Application(), KodeinAware {
    override fun onCreate() {
        super.onCreate()
    }

    override val kodein: Kodein = Kodein.lazy {
        import(ExampleServiceModule())
    }
}
```

### Fragment
以上を設定することで下記のように利用できます。

```kotlin:ExampleFragment.kt
class ExampleFragment: Fragment(), KodeinAware {

    // kodeinオブジェクトを取得するために必ず必要
    override val kodein by closestKodein()
    // バインディングの方法はいろいろあるが今回はトリガー方式で行う
    override val kodeinTrigger = KodeinTrigger()
    // 依存注入先の変数を定義
    val exampleService: ExampleService by instance()


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        // triggerで依存関係のあるインスタンス設定する
        kodeinTrigger.trigger()

        // たったこれだけでインスタンス化したサービスを利用できる！
        exampleService.xxx

        // 略
    }
}
```

# 便利な使い方
それでは本題のOkHttp + Retrofit2で使っていきます。

## gradle
```gradle:app/build.gradle

// 略

dependencies {
    def kodein_version = '5.3.0'

    // 略

    // kodein
    implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
    implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"

    // ---- 追記　start ----
    def okhttp3_version = "3.11.0"
    def retrofit2_version = "2.5.0"
    def autodispose_verion = '1.0.0'
    // network
    implementation "com.squareup.okhttp3:okhttp:$okhttp3_version"
    implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3_version"
    implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
    implementation "com.squareup.retrofit2:converter-moshi:$retrofit2_version"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
    implementation "com.squareup.moshi:moshi:$moshi_verion"
    implementation "com.squareup.moshi:moshi-kotlin:$moshi_verion"

    // RxKotlin
    implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
    // autodispos
    implementation "com.uber.autodispose:autodispose-android-ktx:$autodispose_verion"
    implementation "com.uber.autodispose:autodispose-android-archcomponents-ktx:$autodispose_verion"
    // ---- 追記　end ----
}
```

## Kotlin
[Module](#module)と[Application](#application)は前述の設定そのまま使います。

### ServiceGenerator

```kotlin:ServiceGenerator.kt
object ServiceGenerator {
    fun createRetrofit(context: Context): Retrofit {

        val baseUrl: String = context.getString(R.string.api_base_url)

        // jsonコンバーターにはMoshiを利用。Kotlinのnull安全を保証してくれる
        val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
        val builder: Retrofit.Builder = Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(MoshiConverterFactory.create(moshi))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())

        val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
        httpClient.let {
            // debugするのに通信log見れないと辛いので見れるようにする
            val logging: HttpLoggingInterceptor = HttpLoggingInterceptor()
                    .setLevel(HttpLoggingInterceptor.Level.BODY)
            if (!it.interceptors().contains(logging)) {
                it.addInterceptor(logging)
            }
            it.connectTimeout(20, TimeUnit.SECONDS)
            it.readTimeout(20, TimeUnit.SECONDS)

            // リクエスト前の処理をカスタマイズしたいのでExampleInterceptorを追加する
            val ExampleInterceptor = ExampleInterceptor(context)
            if (!it.interceptors().contains(ExampleInterceptor)) {
                it.addInterceptor(ExampleInterceptor)
            }

            builder.client(it.build())
        }

        val retrofit = builder.build()
        return retrofit
    }
}
```

### ExampleInterceptor

```kotlin:ExampleInterceptor.kt
class ExampleInterceptor(val context: Context) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request: Request = chain.request()
        val  builder: Request.Builder = request.newBuilder()

        builder.addHeader("Accept", "application/json")

        // 今回の例ではすべてのリクエストでアクセストークンを送るようにしています。
        // ですがretrofit2.5.0からはカスタムアノテーションが利用できるようになっており、
        // 送りたいエンドポイントのみヘッダーを付与するなどの場合分けが簡単に出来るようになっています。
        // Sato Shunさんがわかりやすく解説されてます。
        // 合わせて参照してみてください。有益な情報感謝:pray:
        // https://satoshun.github.io/2018/11/retrofit_custom_annotation/
        val accessToken = SharedPref.getAccessToken(context)
        if (!accessToken.isNullOrEmpty()) {
            builder.addHeader("access-token", requireNotNull(accessToken))
            builder.addHeader("client", requireNotNull(SharedPref.getClient(context)))
            builder.addHeader("uid", requireNotNull(SharedPref.getUid(context)))
        }

        val response: Response = chain.proceed(builder.build())

        return response
    }
}
```

### ExampleService

```kotlin:ExampleService.kt
interface ExampleService {

    /**
     * ログイン
     */
    @POST("/api/v1/auth/sign_in")
    fun signin(@Body login: Login): Observable<Response<ResponseBody>>

}
```

### Fragment

```kotlin:Example2Fragment.kt
class Example2Fragment: Fragment(), KodeinAware {

    override val kodein by closestKodein()
    override val kodeinTrigger = KodeinTrigger()
    val exampleService: ExampleService by instance()

    // rxkotlinのストリームを自動的にDisposeするためのスコープを定義。今回の場合はFragmentが終了したらDispose実行するようにする
    protected val scope: LifecycleScopeProvider<Lifecycle.Event> by lazy {
        AndroidLifecycleScopeProvider.from(this)
    }


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        kodeinTrigger.trigger()

        val login = Login(edit_id.text.toString(), edit_pass.text.toString())
        // Observableでretrofitを実行する
        exampleService.signin(login)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .`as`(autoDisposable(scope)) // スコープを設定
                .subscribe(
                        ({success -> run {
                            baseActivity?.let {ba ->
                                if (ba.isDestroyed) return@run

                                if (!success.isSuccessful()) {
                                    ErrorUtils.showErrorSnackbar(success.code(), view, ba)
                                    return@run
                                }

                                // 略
                            }
                        }}),
                        ({error -> run {
                            baseActivity?.let {ba ->
                                if (ba.isDestroyed) return@run
                                ErrorUtils.showErrorSnackbar(error, view, ba)
                            }
                        }})
                )

        // 略        
    }
}
```


# 最後に
いかがでしたでしょうか？今回紹介した方法だけでもかなり便利に使えると思います。他にも便利な使い方出来ますのでぜひ調べてみてください。
皆さんがKodeinを利用するきっかけになれれば幸いです。