Implementation of API call state using sealed class

Implementation of API call state using sealed class

4bbdb0ec71d1f6fd59d2f6fe270d9e4c?s=128

Hideyuki Kikuma

January 16, 2020
Tweet

Transcript

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

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

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

  4. よくある画面表示の要件 • 画面表示には API から取ってきた情報が必要 ◦ 初期表示はデフォルト表示の場合と何も表示したくないパターンどっちもありそう • ロード中は progress

    表示にしたい • API のレスポンスが返ってきたらその内容を表示 • API がエラーだった場合はエラー画面表示にしたい • エラー画面に再読み込みボタンを付けたい
  5. None
  6. つまり画面の状態としてはざっくり4つ • 初期状態 • ロード中 • データ取得完了 • データ取得エラー

  7. メルペイではRemoteDataKを使っている 内部で使っていたライブラリを OSS 化したもの • 状態を表現する sealed class • 便利関数

    (map, mapError など ) メルペイの用途ではこれで満足できている https://github.com/mercari/RemoteDataK
  8. コードで表すとこんな感じ sealed class RemoteData<out V : Any, out E :

    Exception> { object Initial : RemoteData<Nothing, Nothing>() class Loading<V : Any>(progress: Int? = null, val total: Int = 100) : RemoteData<V, Nothing>() class Success<out V : Any>(val value: V) : RemoteData<V, Nothing>() class Failure<out E : Exception>(val error: E) : RemoteData<Nothing, E>() }
  9. 素朴な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<String> )
  10. その場合の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 } }
  11. その場合の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 }
  12. RemoteDataで書き直してみる data class SampleState( val entity: RemoteData<SampleEntity, Exception> = RemoteData.Initial

    ) data class SampleEntity( val title: String, val items: List<String> )
  13. その場合の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 } }
  14. その場合の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 }
  15. あれ? そんなに変わらなくない?

  16. 拡張関数を追加してみる fun <V : Any, E : RemoteError> Result<V, E>.toRemoteData():

    RemoteData<V, E> = when (this) { is Result.Success -> RemoteData.Success(this.value) is Result.Failure -> RemoteData.Failure(this.error) }
  17. 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 } }
  18. View周りでいいこととか statusObservable.map(SampleState::entity) .map { it.value.title } // title が nonnull

    である必要がある .distinctUntilChanged() .subscribe { updateTitle(it) }
  19. 課題もある

  20. 例えばkotlinx.serializationが使えない • AAC の ViewModel で状態管理をしたい場合、 Bundle に入れられる型が必要 • kotlinx.serialization

    で Serializable にしたい • RemoteDataK の Failure は java の Exception を持っている • これは kotlinx.serialization で Serializable に出来ない このため、アドホックなコードで対応してる
  21. 詳しくはこちら https://tech.mercari.com/entry/2019/12/04/100000 https://tech.mercari.com/entry/2019/12/18/100000

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

  23. まとめ • sealed class を使うと状態 + 値の組み合わせをうまく表現できる • 一つのプロパティにまとまるので関数で処理しやすい •

    ロード中などの状態を全体で統一した表現に出来る • NullObject を作らずに nonnull に出来るので Rx と相性がいい
  24. ご静聴ありがとうございました