Dagger2 (android support module)とretrofit2をつかってAPIレスポンスをListViewで表示する

掲題のとおりAndroidのListViewを表示してみる。 APIリクエストは retrofitを使い天気情報を取得できるOpenWeatherMapのAPIを利用する。

github.com

DIにはDaggerを使い、2.11から有効なandroid support moduleを利用する。

github.com

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を継承する流れを紹介するエントリもあるが DaggerApplicationHasActivityInjectorの実装が含まれているのでこちらをつかう。

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コードなどが散見してます。)

github.com

参考