Edited at
AndroidDay 1

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


はじめに

この記事は Android Advent Calendar 2018 の1日目です!

みなさんはDI使っていますか?とても便利なんですが、外部から依存関係を注入…などパッと理解しづらい説明で難しそうというイメージがあり、利用したことない方も多いと思います。

今回はそんなDIの便利な使い方及び、利用されたことがある方にはOkHttp + Retrofit2の便利な使い方をご紹介したいと思います。


Kodeinとは?

KOtlin DEpendency INjection、頭文字をとってKodeinだそうで、DI(依存性注入)ライブラリです。Kotlinに特化しており、Dagger2とは違いKotlinで利用する際にkaptが不要です。

例えば使いまわしたいインスタンスを簡単にシングルトンパターンとして実装することができます。


Kodeinの最小限の設定方法

環境: Android Studio 3.2.1, kotlin 1.3.0, kodein 5.3.0


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


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


ExampleAndroidApplication.kt

class ExampleAndroidApplication : Application(), KodeinAware {

override fun onCreate() {
super.onCreate()
}

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



Fragment

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


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


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

ModuleApplicationは前述の設定そのまま使います。


ServiceGenerator


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


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


ExampleService.kt

interface ExampleService {

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

}



Fragment


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を利用するきっかけになれれば幸いです。