はじめに
この記事は 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
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
// 今回は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
class ExampleAndroidApplication : Application(), KodeinAware {
override fun onCreate() {
super.onCreate()
}
override val kodein: Kodein = Kodein.lazy {
import(ExampleServiceModule())
}
}
Fragment
以上を設定することで下記のように利用できます。
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
// 略
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とApplicationは前述の設定そのまま使います。
ServiceGenerator
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
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
interface ExampleService {
/**
* ログイン
*/
@POST("/api/v1/auth/sign_in")
fun signin(@Body login: Login): Observable<Response<ResponseBody>>
}
Fragment
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を利用するきっかけになれれば幸いです。