下の資料の「今考え中です」について、色々と考えがまとまってきたのでデータクラスを作ってみました。
FailureStatus
というライブラリにしてあります。
FailureStatusについて
どんなクラスか
FailureStatus自体は、Gistにしようかなーと悩んだ程度のクラスです。
enum class Status { SUCCESS, ERROR, LOADING } data class FailureStatus(val message: String? = null, val throwable: Throwable? = null) { constructor(message: String?) : this(message = message, throwable = null) constructor(t: Throwable?) : this(message = null, throwable = t) fun createMessage(throwableFunc: (throwable: Throwable?) -> String): String = if (!message.isNullOrEmpty()) { message } else { throwableFunc(throwable) } } data class NetworkState( val status: Status, val failure: FailureStatus? = null ) { companion object { val LOADED = NetworkState(status = Status.SUCCESS) val LOADING = NetworkState(status = Status.LOADING) fun error(message: String?) = NetworkState(status = Status.ERROR, failure = FailureStatus(message)) fun error(t: Throwable) = NetworkState(status = Status.ERROR, failure = FailureStatus(t)) } }
Status
- 通信の結果を含めた、通信状況を表すenum
FailureStatus
** エラー文言、もしくはエラー原因を保持するデータクラス- エラー原因からエラー文言を作詞するメソッドを持つ
NetworkState
Status
とFailureStatus
を持つデータクラスLOADED
,LOADING
,error()
を主に利用することを想定
のような想定で作っています。
やりたいこと
View層で通信状態や通信結果を表示する時、複数のLiveData
を使わずにViewModel
から通信状態を伝播させることがモチベーションです。
下記のようなViewModel
を書いておきたいという感じですね。
class UserViewModel : ViewModel() { val repository = UserRepository() val user = MutableLiveData<User>() val network = MutableLIveData<NetworkState>() fun fetchUser() { viewModelScope.launch { network.postValue(NetworkState.LOADING) runCatching { repository.fetchUser() }.fold { onSuccess = { user.postValue(it) network.postValue(NetworkState.LOADED) }, onFailure = { network.postValue(NetworkState.error(it)) } } } }
View側(Activity'や
Fragment)では、それぞれ
Userが取得したい時には
UserViewModel.userをsubscribeし、ネットワークの状況を取得したい時には
UserViewModel.networkをsubscribeします。
この時、通信レスポンスにエラーメッセージが含まれる場合などには
network.postValue(NetworkState.error(it.message))` のようにStringを取り出すことができます。
onSuccess = { if (it.isSuccess()) { user.postValue(it) network.postValue(NetworkState.LOADED) } else { network.postValue(NetworkState.error(it.message)) } },
表示をどうするか
エラーケースをカバーするのであれば、ある程度表示のメソッドも共通化したいなと思います。 そこで、簡単なExtensionsを作成しました。
fun Activity.networkErrorBar( anchor: View, status: FailureStatus, message: (throwable: Throwable?) -> String, duration: Int = Snackbar.LENGTH_SHORT, isShowAction: Boolean = true, actionMessage: (throwable: Throwable?) -> String = { getString(android.R.string.ok) }, action: (throwable: Throwable?) -> Unit = {} ) { val bar = Snackbar.make(anchor, status.createMessage(message), duration) if (isShowAction) { bar.setAction(actionMessage(status.throwable)) { action(status.throwable) } } bar.show() }
message: (throwable: Throwable?) -> String
に、想定されるThrowable
に対応する文字列を(つまりエラーケースに対応する文言を)返す関数を引き渡すことで、エラー表示文言を切り替えられる仕組みになっています。
もちろん、完全に想定外のケースではwhen
文のelse句などを使うことを想定しています。
これらの関数を作成すると、逆に依存関係が増えてめんどいかなーという気もしています。 その場合には、上のFailureStatusクラスだけ活用してもらえればいいのかなと思っています。
追記 (2019年4月16日)
読み直していたら、NetworkStateは下の書き方の方がいい気がしてきました。
sealed class NetworkState(val status: Status) { object LOADED: NetworkState(Status.SUCCESS) object LOADING: NetworkState(Status.LOADING) data class ERROR(val error: FailureStatus): NetworkState(Status.ERROR) { constructor(throwable: Throwable?) : this(FailureStatus(throwable)) constructor(message: String?) : this(FailureStatus(message)) } }