rx-preferencesをつかってSharedPreferencesの更新をSubscribeする


ブログエントリで試しているAndroidアプリにFluxアーキテクチャを導入をしたことでデータの流れがStreamになった。SharedPreferencesも rx-preferencesをつかえば更新状態をSubscribeできるので導入過程をまとめていく。

お試し中のアプリはOpenWeatherMapのAPIを使い天気情報を取得する。ZipCodeをクエリに追加すれば地域の天気情報が取得できる。アプリ内でZipCodeを登録できるようにしたいのでSharedPreferencesで管理することにする。

rx-preferencesをつかう

rx-preferencesを追加してRepository化するまで。

1
2
3
# dependencies.gradle

rxPreferences = 'com.f2prateek.rx.preferences2:rx-preferences:2.0.0-RC3'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# SettingsRepository.kt

@Singleton
class SettingsRepository @Inject constructor(private val application: Application) {

    private val SETTING_NAME = "setting"
    private val ZIP_CODE_NAME = "zip_code"

    fun getZipCode() = getRxSharedPreferences(SETTING_NAME).getString(ZIP_CODE_NAME)
    fun updateZipCode(zipCode: String) = getZipCode().set(zipCode)

    private fun getRxSharedPreferences(name: String) = RxSharedPreferences.create(
            application.getSharedPreferences(BuildConfig.APPLICATION_ID + '.' + name, Context.MODE_PRIVATE))
}

Repository層の役割はデータ取得や更新など。Repositoryを使う側はその先がAPIかDBかに関心する必要させたくないのでSharedPreferencesのデータ取得や更新もRepository層で実装した。
このRepositoryをFluxアーキテクチャに乗せていく。

ActionとStoreをつくる。Dispatcherはつくらない。

1
2
3
4
5
6
7
# SettingsAction.kt

@Singleton
class SettingsAction @Inject constructor(private val settingsRepository: SettingsRepository) {

    fun updateZipCode(zipCode: String) = settingsRepository.updateZipCode(zipCode)
}

ActionはZipCodeを更新するメソッドを追加した。

1
2
3
4
5
6
7
8
9
# SettingsStore.kt

@Singleton
class SettingsStore @Inject constructor(private val settingsRepository: SettingsRepository) {
    fun zipCode() = settingsRepository.getZipCode()
            .asObservable()
            .subscribeOn(Schedulers.io())
            .toFlowable(BackpressureStrategy.LATEST)
}

StoreにはZipCodeの状態をObserveする zipCode()メソッドを追加した。

rx-preferencesは asObservable()を呼び出せばStreamオブジェクトを取得できる。BackpressureStrategy.LATESTを有効にすることで常に最新の値が流れるようにした。

Fluxアーキテクチャに乗るならばDispatcherを作るところだが、 rx-preferencesがStreamを提供してくれるのでこのエントリではDispatcherを追加していない。このアプリはまだ小さいのでDispatcherの追加はしない判断をしたが大きなアプリであればデータのアクションに応じて専用のDispatcherに乗せたほうが良い場合もあると思う。

ZipCodeの更新をSubscribeする

FragmentでZipCodeの更新をSubscribeする。

 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
32
33
34
35
# ForecastsFragment.kt

class ForecastsFragment : AutoDisposeFragmentKotlin() {

    // ---

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        forecastsStore.forecasts()
                .observeOn(AndroidSchedulers.mainThread())
                .`as`(autoDisposable(this))
                .subscribe { forecasts ->
                    cityView.text = "%s/%s".format(forecasts.city.name, forecasts.city.country)
                    listView.adapter = ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1,
                            forecasts.list.map {
                                "%s - %s %s/%s".format(
                                        DateUtils.formatDateTime(activity, it.dt * 1000L, FORMAT_NO_YEAR),
                                        it.weather.get(0).main, it.temp.min, it.temp.max)
                            })
                }

        savedInstanceState ?: settingsStore.zipCode()
                .observeOn(AndroidSchedulers.mainThread())
                .`as`(AutoDispose.autoDisposable(this))
                .subscribe {
                    if (it.isNotBlank()) {
                        forecastsAction.findByDaily(it)
                    } else {
                        errorAction.onError("You must to set ZipCode.")
                    }
                }
    }

    // ---
}

SettingActivityでZipCodeが更新されると settingsStore.zipCode()に最新のZipCodeが流れてくるのでSubscribeをしてAPIを呼びだしている。
forecastsAction.findByDaily(it)が呼び出されれば、 forecastsStore.forecasts()に最新のAPI結果が流れてくるのでViewが切り替わる。

コード

rx-preferencesの導入前と後のコード比較ができるようにPRを残しています。 エントリで紹介したコードは断片的なので参考になれば嬉しいです。

https://github.com/soushin/sunshine-app/pull/4github.com

まとめ