The Power of LiveData & Kotlin Sealed Classes
Quentin Colle
Quentin Colle

The Power of LiveData & Kotlin Sealed Classes

Quentin Colle, Senior Android Engineer

Since Kotlin came out, I have been excited by what Sealed classes bring to the table. When building views, we usually encounter the same states over and over again. This post posited a solution using Sealed classes, and I wanted to give it a shot. I am also fond of the observer pattern and LiveData and thought combining the two could create a very powerful mechanism to fetch data for my repository layer.

I decided to explore the concept using a “Cart” model, which is used in eCommerce apps to represent a shopping cart of a user:

data class Cart(val price: Double?, ...) { ... }

We create our Sealed class that represents a network-bound resource:

/**
 * Represents a network bound resource. Each subclass represents the resource's   state:
 * - [Loading]: the resource is being retrieved from network.
 * - [Success]: the resource has been retrieved (available in [Success.data] field)
 * - [Failure]: the resource retrieving has failed (throwable available in [Failure.throwable]
 * field)
*/
sealed class Resource<out T> {
 class Loading<out T> : Resource<T>()
 data class Success<out T>(val data: T) : Resource<T>()
 data class Failure<out T>(val throwable: Throwable) : Resource<T>()
}

When fetching data, we generally encounter 3 states: an error state, loading state and success state. These are symbolized in this Sealed class by Loading, Failure and Success. Using generics allows us to reuse this class across the app for multiple sets of data.

Manager

We abstract the management of the Cart by using a class extending LiveData. This will allow us to notify any observers of changes to the resource, whether the data is loading, or if an error occurred.

/**
 * Observable manager for saving the cart's resource information.
 */
class CartManager : LiveData<Resource<Cart?>>() {

 init {
   value = Success(null)
 }

 /**
  * Set the [Cart] value and notifies observers.
  */
 internal fun set(cart: Cart) {
   resource = Success(cart)
   postValue(resource)
 }

 internal fun clear() {
   resource = Success(null)
   postValue(resource)
 }

 internal fun loading() {
   resource = Loading()
   postValue(resource)
 }

 internal fun error(t: Throwable) {
   resource = Failure(t)
   postValue(resource)
 }
}

Repository

We then integrate the manager with our repository layer, abstracted from the view:

/**
 * [Cart] repository class in charge of providing api for getting and updating a cart data.
 */
class CartRepository(private val service: RetrofitApi, private val cartManager: CartManager) {

 fun getCartResource(): LiveData<Resource<Cart?>> = cartManager

 /**
  * Get a [Cart].
  */
 suspend fun get() {
   withContext(Dispatchers.IO) {
     try {
       cartManager.loading()

       val cart = service.getCart().await()

       cartManager.set(cart)
     } catch (e: Exception) {
       cartManager.error(e)
     }
   }
 }

 /**
  * Add a [Cart] shipping information.
  */
 suspend fun addShippingAddress(shipping: ShippingAddress) {
   withContext(Dispatchers.IO) {
     try {
       cartManager.loading()

       val cart = service.addShippingAddress(shipping).await()

       cartManager.set(cart)
     } catch (e: Exception) {
       cartManager.error(e)
     }
   }
 }
}

ViewModel & View

You can then use the repository for your view:

class CheckoutViewModel(repo: CartRepository) : ViewModel() {
 /**
  * Live data for [Cart] information.
  */
 val cart: LiveData<Resource<Cart?>> = repo.getCartResource()
}

And in your view:

class CheckoutActivity : AppCompatActivity() {

 private lateinit var vm: CheckoutViewModel

 override fun onCreate(savedInstanceState: Bundle?) {
   // Initialization
   ...
   vm.cart.observe(this, Observer { resource ->
     when (resource) {
       is Loading -> showLoading()
       is Success -> {
         displayCart(resource.data) // Do something with your data.
         hideLoading()
       }
       is Failure -> {
         showError(resource.throwable) // Do something with your error.
         hideLoading()
       }
   })
 }
}

Voilà! You can use the resource in any way you like.

In conclusion we used:

  • A LiveData (CartManager) to propagate to any views subscribed to it the most up to date value.
  • A Sealed class (Resource) to decouple the network states, for the view to update its content to.

For reference, you can find the code here!

Thanks for reading,