16
6

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

AndroidAdvent Calendar 2018

Day 1

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

Last updated at Posted at 2018-11-30

はじめに

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

16
6
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?