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


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

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

APIをリクエストするServiceクラスをつくる

1
2
3
4
5
interface OpenWeatherMapService {

    @GET("/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7&APPID=XXXXX")
    fun findForecastByDaily(): Observable<Forecasts>
}

このServiceクラスをRepositoryクラスから呼び出し見通しの良いコードにするためにDIを利用していく。DIについては後述する。

Parcelableを実装したDTO(data class)

1
2
3
4
5
6
7
8
9
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を用意する。 フィールドにプリミティブ型ではないオブジェクト型を使う場合は次のようにする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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をつくる

1
2
3
class OpenWeatherMapRepository(val openWeatherMapService: OpenWeatherMapService) {
    fun findForecastByDaily() = openWeatherMapService.findForecastByDaily()
}
1
2
3
4
5
6
7
8
9
@Module
internal object RepositoryModule {

    @Provides
    @Singleton
    @JvmStatic
    fun provideOpenWeatherMapRepository(openWeatherMapService: OpenWeatherMapService) =
            OpenWeatherMapRepository(openWeatherMapService)
}

OpenWeatherMapRepositoryを提供するRepositoryModuleをつくった。

DataModuleをつくる

RepositoryModuleをIncludeしたDataModuleをつくる。このモジュールでRetrofitクライアントをビルドする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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)
}

@ContributesAndroidInjectorをつかいMainActivityへのInjectを定義する

Dagger 2.11の重要ポイントの1つ。ActivityへのInjectは @ContributesAndroidInjectorをつかいUiModuleをつくる。

1
2
3
4
5
6
@Module
internal abstract class UiModule {

    @ContributesAndroidInjector
    internal abstract fun contributeMainActivity(): MainActivity
}

Activityが増えたときには、ここにActivityへのInjectを追加する。

ApplicationComponentに AndroidInjector を継承させる

Dagger 2.11の重要ポイントの1つ。ApplicationクラスへInjectさせるためにApplicationComponentに AndroidInjector<KotlinApplication> を継承させる。後述するApplicationの親クラスに dagger.android.support.DaggerApplicationを使うためmoduleにAndroidSupportInjectionModule を追加する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Singleton
@Component(modules = arrayOf(AndroidSupportInjectionModule::class,
        AppModule::class,
        DataModule::class,
        UiModule::class))
interface ApplicationComponent : AndroidInjector&lt;KotlinApplication> {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun application(application: KotlinApplication): Builder
        fun build(): ApplicationComponent
    }

    override fun inject(application: KotlinApplication)
}

Applicationクラスに DaggerApplicationを継承させ実装する

HasActivityInjectorを継承する流れを紹介するエントリもあるが DaggerApplicationHasActivityInjectorの実装が含まれているのでこちらをつかう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)を呼び出す。

1
2
3
4
5
6
7
8
9
class MainActivity : AppCompatActivity() {

    @Inject lateinit var openWeatherMapRepository: OpenWeatherMapRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)

    // -
}

APIレスポンスをListViewに表示する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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&lt;ListView>(R.id.listview).let { view ->
                        view.adapter = ArrayAdapter&lt;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をつかって日にちと最高気温と最低気温を表示している。

まとめ

コード

このエントリまでのコードがPull Requestにまとまっていますので参考になれば嬉しいです。(初回のコミットなので不要なlayoutコードなどが散見してます。)

参考