Slide 1

Slide 1 text

API通信の状態を sealed classを使って表現する Bonfire Android #6

Slide 2

Slide 2 text

自己紹介 @hidey / 菊間 英行 株式会社メルペイ Android エンジニア DroidKaigi スタッフ

Slide 3

Slide 3 text

API通信中の状態とか 困ったりしてませんか?

Slide 4

Slide 4 text

よくある画面表示の要件 ● 画面表示には API から取ってきた情報が必要 ○ 初期表示はデフォルト表示の場合と何も表示したくないパターンどっちもありそう ● ロード中は progress 表示にしたい ● API のレスポンスが返ってきたらその内容を表示 ● API がエラーだった場合はエラー画面表示にしたい ● エラー画面に再読み込みボタンを付けたい

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

つまり画面の状態としてはざっくり4つ ● 初期状態 ● ロード中 ● データ取得完了 ● データ取得エラー

Slide 7

Slide 7 text

メルペイではRemoteDataKを使っている 内部で使っていたライブラリを OSS 化したもの ● 状態を表現する sealed class ● 便利関数 (map, mapError など ) メルペイの用途ではこれで満足できている https://github.com/mercari/RemoteDataK

Slide 8

Slide 8 text

コードで表すとこんな感じ sealed class RemoteData { object Initial : RemoteData() class Loading(progress: Int? = null, val total: Int = 100) : RemoteData() class Success(val value: V) : RemoteData() class Failure(val error: E) : RemoteData() }

Slide 9

Slide 9 text

素朴なstate実装 data class SampleState( val entity: SampleEntity? = null, val error: Exception? = null, val isLoading: Boolean = false ) data class SampleEntity( val title: String, val items: List )

Slide 10

Slide 10 text

その場合のStateの変更箇所のコード class SampleReducer { fun reduce(action: Action, currentState: SampleState): SampleState = when (action) { is ShowDataAction -> { if (action.result.isSuccess) { currentState.copy(entity = action.result.entity, isLoading = false) } else { currentState.copy(error = action.result.error, isLoading = false) } } is LoadData -> currentState.copy(isLoading = true, entity = null, error = null) else -> currentState } }

Slide 11

Slide 11 text

その場合のView周りのコード fun updateView(state: SampleState) { if (state.entity != null) { titleView.text = state.entity.title adapter.items = state.entity.items } if (state.error != null) { errorMessage.text = state.error.localizedMessage } errorView.isVisible = state.error != null loadingView.isVisible = state.isLoading }

Slide 12

Slide 12 text

RemoteDataで書き直してみる data class SampleState( val entity: RemoteData = RemoteData.Initial ) data class SampleEntity( val title: String, val items: List )

Slide 13

Slide 13 text

その場合のStateの変更箇所のコード class SampleReducer { fun reduce(action: Action, currentState: SampleState): SampleState = when (action) { is ShowDataAction -> { if (action.result.isSuccess) { currentState.copy(entity = RemoteData.Success(action.result.entity)) } else { currentState.copy(entity = RemoteData.Failure(action.result.error)) } } is LoadData -> currentState.copy(entity = RemoteData.Loading()) else -> currentState } }

Slide 14

Slide 14 text

その場合のView周りのコード fun updateView(state: SampleState) { when (val entity = state.entity) { is RemoteData.Success -> { titleView.text = entity.value.title adapter.items = entity.value.items } is RemoteData.Failure -> errorMessage.text = entity.error.localizedMessage } errorView.isVisible = state.entity.isFailure loadingView.isVisible = state.entity.isLoading }

Slide 15

Slide 15 text

あれ? そんなに変わらなくない?

Slide 16

Slide 16 text

拡張関数を追加してみる fun Result.toRemoteData(): RemoteData = when (this) { is Result.Success -> RemoteData.Success(this.value) is Result.Failure -> RemoteData.Failure(this.error) }

Slide 17

Slide 17 text

Stateの変更箇所のコード class SampleReducer { fun reduce(action: Action, currentState: SampleState): SampleState = when (action) { is ShowDataAction -> currentState.copy( entity = action.result.toRemoteData() ) is LoadData -> currentState.copy(entity = RemoteData.Loading()) else -> currentState } }

Slide 18

Slide 18 text

View周りでいいこととか statusObservable.map(SampleState::entity) .map { it.value.title } // title が nonnull である必要がある .distinctUntilChanged() .subscribe { updateTitle(it) }

Slide 19

Slide 19 text

課題もある

Slide 20

Slide 20 text

例えばkotlinx.serializationが使えない ● AAC の ViewModel で状態管理をしたい場合、 Bundle に入れられる型が必要 ● kotlinx.serialization で Serializable にしたい ● RemoteDataK の Failure は java の Exception を持っている ● これは kotlinx.serialization で Serializable に出来ない このため、アドホックなコードで対応してる

Slide 21

Slide 21 text

詳しくはこちら https://tech.mercari.com/entry/2019/12/04/100000 https://tech.mercari.com/entry/2019/12/18/100000

Slide 22

Slide 22 text

今日のサンプルコード https://github.com/hidey/remote_data_sample

Slide 23

Slide 23 text

まとめ ● sealed class を使うと状態 + 値の組み合わせをうまく表現できる ● 一つのプロパティにまとまるので関数で処理しやすい ● ロード中などの状態を全体で統一した表現に出来る ● NullObject を作らずに nonnull に出来るので Rx と相性がいい

Slide 24

Slide 24 text

ご静聴ありがとうございました