Dagger2 (android support module)とretrofit2をつかってAPIレスポンスをListViewで表示する
掲題のとおりAndroidのListViewを表示してみる。
APIリクエストは retrofit
を使い天気情報を取得できるOpenWeatherMapのAPIを利用する。
DIにはDaggerを使い、2.11から有効なandroid support moduleを利用する。
APIをリクエストするServiceクラスをつくる
interface OpenWeatherMapService { @GET("/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7&APPID=XXXXX") fun findForecastByDaily(): Observable<Forecasts> }
- レスポンスの型は
Observable<Forecasts>
。型パラメータのForecastsはParcelable
を実装したDTO。 APPID=XXXXX
はopenweathermapから取得したID
このServiceクラスをRepositoryクラスから呼び出し見通しの良いコードにするためにDIを利用していく。DIについては後述する。
Parcelableを実装したDTO(data class)
data class Forecasts(var cod: Int, var list: List<Forecast>) : Parcelable { constructor(src: Parcel) : this( cod = src.readInt(), list = src.createTypedArrayList(Forecast.CREATOR) ) // - }
ActivityやFragmentにパラメータを渡すために Parcelable
を実装したdata classを用意する。
フィールドにプリミティブ型ではないオブジェクト型を使う場合は次のようにする。
data class Forecast(var dt: Long, var temp: Temp, var weather: List<Weather>) : Parcelable { constructor(src: Parcel) : this( dt = src.readLong(), temp = src.readParcelable(Temp::class.java.classLoader), // ← data class `Temp` weather = src.createTypedArrayList(Weather.CREATOR) // ← List型のパラメータに data class `Weather` ) override fun describeContents(): Int { return 0 } override fun writeToParcel(dest: Parcel?, flags: Int) { dest?.writeLong(dt) dest?.writeParcelable(temp, flags) // ← data class `Temp` dest?.writeList(weather) // ← List型のパラメータに data class `Weather` } // - }
MainActivityでDIする
MainActivityでOpenWeatherMapService
を提供するRepositoryクラスをInjectするまでの過程をまとてめていく。
RepositoryModuleをつくる
class OpenWeatherMapRepository(val openWeatherMapService: OpenWeatherMapService) { fun findForecastByDaily() = openWeatherMapService.findForecastByDaily() }
@Module internal object RepositoryModule { @Provides @Singleton @JvmStatic fun provideOpenWeatherMapRepository(openWeatherMapService: OpenWeatherMapService) = OpenWeatherMapRepository(openWeatherMapService) }
OpenWeatherMapRepository
を提供するRepositoryModuleをつくった。
DataModuleをつくる
RepositoryModuleをIncludeしたDataModuleをつくる。このモジュールでRetrofitクライアントをビルドする。
@Module(includes = arrayOf(RepositoryModule::class)) internal object DataModule { @Provides @Singleton @JvmStatic fun provideMoshi() = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() @Provides @Singleton @JvmStatic fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder() .build() @Provides @Singleton @JvmStatic fun provideRetrofit(oktHttpClient: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder() .client(oktHttpClient) .baseUrl("http://api.openweathermap.org") .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build() @Provides @Singleton @JvmStatic fun provideOpenWeatherMapService(retrofit: Retrofit) = retrofit.create(OpenWeatherMapService::class.java) }
- JSONパーサにはKotlinサポートが入っているMoshiをつかう
@ContributesAndroidInjectorをつかいMainActivityへのInjectを定義する
Dagger 2.11の重要ポイントの1つ。ActivityへのInjectは @ContributesAndroidInjector
をつかいUiModuleをつくる。
@Module internal abstract class UiModule { @ContributesAndroidInjector internal abstract fun contributeMainActivity(): MainActivity }
Activityが増えたときには、ここにActivityへのInjectを追加する。
ApplicationComponentに AndroidInjector<KotlinApplication>
を継承させる
Dagger 2.11の重要ポイントの1つ。ApplicationクラスへInjectさせるためにApplicationComponentに AndroidInjector<KotlinApplication>
を継承させる。後述するApplicationの親クラスに dagger.android.support.DaggerApplication
を使うためmoduleにAndroidSupportInjectionModule
を追加する。
@Singleton @Component(modules = arrayOf(AndroidSupportInjectionModule::class, AppModule::class, DataModule::class, UiModule::class)) interface ApplicationComponent : AndroidInjector<KotlinApplication> { @Component.Builder interface Builder { @BindsInstance fun application(application: KotlinApplication): Builder fun build(): ApplicationComponent } override fun inject(application: KotlinApplication) }
Applicationクラスに DaggerApplication
を継承させ実装する
HasActivityInjector
を継承する流れを紹介するエントリもあるが DaggerApplication
はHasActivityInjector
の実装が含まれているのでこちらをつかう。
class KotlinApplication : DaggerApplication() { override fun applicationInjector() = DaggerApplicationComponent.builder() .application(this) .build() override fun onCreate() { super.onCreate() } }
MainActivityにOpenWeatherMapRepositoryをInjectする
最後にMainActivityにOpenWeatherMapRepositoryをInjectする。Dagger 2.11の重要ポイントの1つ。InjectするためにはonCreateでAndroidInjection.inject(this)
を呼び出す。
class MainActivity : AppCompatActivity() { @Inject lateinit var openWeatherMapRepository: OpenWeatherMapRepository override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) // - }
APIレスポンスをListViewに表示する
class MainActivity : AppCompatActivity() { @Inject lateinit var openWeatherMapRepository: OpenWeatherMapRepository override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) openWeatherMapRepository.findForecastByDaily() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { forecasts -> findViewById<ListView>(R.id.listview).let { view -> view.adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, forecasts.list.map { "%s - %s %s/%s".format( DateUtils.formatDateTime(this, it.dt * 1000L, FORMAT_NO_YEAR), it.weather.get(0).main, it.temp.min, it.temp.max) }) } } } }
ListのItemViewには simple_list_item_1
をつかって日にちと最高気温と最低気温を表示している。
まとめ
- Daggert2(android support module)のDIをまとめた。android support module以前のDI方法だとコピペコードが増える懸念があり登場した経緯を知ってなるほど、と思った。
- retrofitはシンプルな使い方までに留まっているので引き続き触っていきながら知見をまとめていきたい。
コード
このエントリまでのコードがPull Requestにまとまっていますので参考になれば嬉しいです。(初回のコミットなので不要なlayoutコードなどが散見してます。)