Modeling ViewModel States Using Kotlin’s Sealed Classes

Introduction
“Sealed classes are used for representing restricted class hierarchies…”
As such, Sealed Classes are useful when modeling state’s within various app workflows
We’ve leveraged this to simplify a few different use cases in our app, and wanted to share what we’ve found.
Our Problem
We commonly implement an MVVM pattern for new screens. Generally, we create a ViewModel class and use DataBinding to bind our ViewModel data into the UI.
This works great for persistent UI state changes where the ViewModel state and UI state stay in lock step until something acts on the view model.
But what about 1-off events? Things that don’t persist over time, or don’t necessarily represent a new UI state?
For us, these tend to fall into a couple of categories
- transient UI messages (usually shown as a Toast or Snackbar)
- handling requests to move to and from screens
We’ve found that we can model these workflows as simple state machines, and kotlin’s sealed classes can help
Our Solution
We’ve identified several state types that we then enumerate using Sealed Classes. We subscribe to these events within a when(state){...}
expression and handle each state appropriately.
Using Kotlin’s sealed classes helps
- define and encapsulate these states
- allow us to create instances of these states with their own data
- provide type checking as states are modified in the future; helping ensure we handle all states
Below, we’ll take a look at a couple examples of how we’ve used this pattern.
Transient UI Messages
1-off messages such as Toasts and Snackbars can be nicely modeled this way, allowing a ViewModel to indicate that different message types can be shown without having to set up different UI bindings for each individual message.
Additionally, by using a pub/sub model for the states, they are more transient in nature than If we were using a data binding field to bind the messages.
We’ll start with Toasts since they are the simple case.
We’ve defined a ShortToast
and LongToast
class that can be published from the ViewModel to show different duration Toast messages.
ToastData
Toast is okay, but let’s try something more exciting; Snacks
SnackData
SnackBars are a convenient way of showing a quick, transient message that also may have an associated action.
Handling the message and the possible action is handled nicely using this pattern by encapsulating the message content, and the action in the state class.
The handling BaseActivity
then binds to the actual SnackBar and handles listening for the UI state changes of the actual view.
ViewModel Lifecycle
We also have found that representing state transitions to/from screens can be handled nicely with this Sealed Class pattern.
We’ve defined a number of ViewModel lifecycle states than are then handled using the same pub/sub model as before.
Our BaseActivity
subscribes and handles the indicated state transitions, and because we used Sealed Classes we get nice IDE messages if we aren’t handling all the states.
Because Sealed Classes also can contain their own state, we can include things like results codes, request codes, or extras with our state transitions which helps give our ViewModels as much control of these transitions as possible.
Challenges & Open Questions
- the current naming is in its own way tied to the framework which goes against our idea of a nice clean mvvm
- ideally would have the state classes be completely framework-independent to improve testability. We went down this path, and found we started to reach diminishing returns where the work required to avoid using
Intent
andBundle
classes was more than we wanted to support - this method requires us to have some type of “bind viewmodel” function in our
BaseActivity
that subscribes to the appropriate state subjects. It’s not a big hit, but is an extra boilerplate step beyond what is handled by DataBinding. We could possibly create a custom BindingAdapter that could handle this boilerplate for us, but haven’t tried it yet.
Further Reading
Conclusion
Kotlin’s Sealed Classes are a useful tool for modeling restricted states within your various app flows. Restrictive compilation rules and IDE checks help ensure new states are handled in the future, and their ability to hold their own state makes them useful for representing states that can hold different values such as transient UI messages.
We’ve found these examples to be useful, and hope you might find the same.
Want to learn more about Android development?
Check out our Android development course offerings
Want to learn more about Kotlin?
Follow Us
For more from the engineers and data scientists building Udacity, follow us here on Medium.
Interested in joining us @udacity? See our current opportunities.