How to make sealed classes generic in kotlin?

Viewed 12931

Is it possible to use the AsyncResult class below to prevent redefining InFlight, Error and InFlight in UserDataAppResult and CreateUserResult?

//TODO: use this to make the below classes generic?
sealed class AsyncResult{
  object InFlight : AsyncResult()
  data class Error(val errorMessage: String) : AsyncResult()
  data class Loaded<out T>(val users: T) : AsyncResult()
}

sealed class UserDataAppResult : AppResult() {
  object InFlight : UserDataAppResult()
  data class Error(val errorMessage: String) : UserDataAppResult()
  data class Loaded(val users: List<User>) : UserDataAppResult()
}

sealed class CreateUserResult : AppResult() {
  object InFlight : CreateUserResult()
  data class Error(val errorMessage: String) : CreateUserResult()
  data class Loaded(val users: User) : CreateUserResult()
}

Is it possible for the above code to look like this?

sealed class AsyncResult{
  class InFlight : AsyncResult()
  data class Error(val errorMessage: String) : AsyncResult()
  data class Loaded<out T>(val users: T) : AsyncResult()
}

sealed class UserDataAppResult : AsyncResult()
sealed class CreateUserResult : AppResult()

val activeUsers: Flowable<UserDataAppResult> = appDatabase.userDao().getActiveUsers(appSettings.currentLanguage.ordinal)
    .map<UserDataAppResult> { UserDataAppResult.Loaded(it) }
    .onErrorReturn { UserDataAppResult.Error(it.localizedMessage) }
    .startWith(UserDataAppResult.InFlight)
    .observeOn(AndroidSchedulers.mainThread())
    .share()

fun createUser(): Flowable<CreateUserResult> {

  val userId = UUID.randomUUID().toString()
  val user = User()
  user.id = userId
  return appDatabase.userDao().insertAll(user)
      .map <CreateUserResult> { CreateUserResult.Loaded(user) }
      .onErrorReturn { CreateUserResult.Error(it.localizedMessage) }
      .startWith(CreateUserResult.InFlight)
}

Currently UserDataAppResult.Error is not found which makes sense. But is it possible to reuse the AppResult sealed class hierarchy and introduce new types.

4 Answers

your Object can't have a generic type in Kotlin but this could be solved simply by following the example below:

sealed class ResponseState<out T> {
    object Loading : ResponseState<Nothing>()
    data class Error(val throwable: Throwable) : ResponseState<Nothing>()
    data class Success<T>(val item: T) : ResponseState<T>()
}

writing:

val _state = MutableLiveData<ResponseState<MessageModle>>() 


_state.postValue(ResponseState.Loading)

myNetworkCall { response, e
  if (e != null) _state.postValue(ResponseState.Error(e))
  else _state.postValue(ResponseState.Success(response))
}

reading:

state.observe(..., {state -> 
  when(state) {
    Loading -> showLoading()
    is Error -> showError(state.throwable)
    is Success -> onSuccess(state.item)
  }
}

Via the Google Guidelines: https://developer.android.com/jetpack/docs/guide

sealed class Resource<T>(
        val data: T? = null,
        val message: String? = null
) {
    class Success<T>(data: T) : Resource<T>(data)
    class Loading<T>(data: T? = null, var refreshing: Boolean = false) : Resource<T>(data)
    class Error<T>(data: T? = null, message: String) : Resource<T>(data, message)
}

Inspired by solution from @kosh

-> ViewState:

sealed class ViewState<out T> {
    object Loading : ViewState<Nothing>()
    data class Error(val throwable: Throwable) : ViewState<Nothing>()
    data class Success<T>(val item: T) : ViewState<T>()
}

-> inside ViewModel:

private val _homeVS = MutableLiveData<ViewState<HomeMode>>()
val homeVS: LiveData<ViewState<HomeMode>> get() = _homeVS

// start requesting API 
_homeVS.value = ViewState.Loading
try {
    val result = loadData()
    _homeVS.value = ViewState.Success(result)
} catch (e: Exception) {
    _homeVS.value = ViewState.Error(e)
}

Then you can use this generic in layout/View

-> View:

viewModel.homeVS.observe(viewLifecycleOwner, {state ->
        when(state) {
            is ViewState.Error -> showError(state.throwable)
            is ViewState.Success -> onSuccess(state.item)
            ViewState.Loading -> showLoading()
        }
    })

-> On layout we may need little mor tuning

sealed class ViewState<out T> {
    object Loading : ViewState<Nothing>()
    data class Error(val throwable: Throwable) : ViewState<Nothing>()
    data class Success<T>(val item: T) : ViewState<T>()

    fun toData(): T? = (this as? Success)?.item
}

toData provides data only onSuccess

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text='@{vm.homeVS.toData() != null ? vm.homeVS.toData().param1 : ""}' />

    <!--onLoading-->
    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility='@{vm.homeVS instanceof ViewState.Loading ? View.VISIBLE : View.GONE}' />

    <!--onError-->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility='@{vm.homeVS instanceof ViewState.Error ? View.VISIBLE : View.GONE}' />

Surely with BindingAdapter, you can make it even better. Here just for illustrating solution.

Good luck,'.

Related