How Circuit makes retaining UI state with Compose effortless

This is a quick post to highlight a feature in Circuit which I really like, along with a quick deep-dive into some recent updates for that feature. For those that don’t know: Circuit is a framework for building apps, written in, and for Compose, and I’m a big fan 📣.

One of the neat things about Circuit is that it does away with complex ViewModel lifecycle. You create presenters which have the same lifecycle as the UIs. This might be ringing alarm bells in your head, probably because of all the knowledge and guidance which you’ve been ingesting and using over the years, especially around ViewModels:

That advice isn’t wrong of course, but it’s only really exists because of the characteristics of ViewModel. With Circuit, the issues listed above goes away 🌬️. The lifecycle of the presenter is the same as the UI, therefore there’s really no need think about lifecycle. The presenter is a composable function, so all you need to think about is: am I being composed?

If you’re interested in learn more about Circuit, the tutorial is a great place to start:

Tutorial - Circuit

Retaining state

One of the great things which ViewModels do is allow retaining state beyond the lifetime of the UI. You don’t want presenters fetching everything each time the UI is created, initially showing empty states, etc. This is where Circuit’s retained state comes in. Retained state is a Circuit concept which allows you to keep state around, beyond the lifetime of the presenter. The presenter can recall the retained value when it is later recreated.

To use retained state, you use the rememberRetained function, which is works exactly like a normal Compose remember:

class ExamplePresenter(...) : Presenter<FooState> {
  @Composable 
  override fun present(): State {
    // Once the presenter is destroyed, the value of `coffeeCost`
    // will be retained. The next time the presenter is created,
    // it's initial value will be set to whatever the retained value
    // is
    val coffeeCost by rememberRetained {
      mutableStateOf<Currency?>(null)
    }
    
    // something updates coffeeCost
    
    return FooState(cost = coffeeCost)
  }
}

The big difference between remember and rememberRetained is that it stores the value somewhere else in memory when the presenter is removed from composition (i.e. stopped). In that same scenario, remember would forget the value for it to be garbage collected.

Saveable

For the keen eyed readers, you might be thinking that saveable does something similar. Why not just use that?

Indeed, saveable is similar in its goals, but it is meant for ‘saving’ state somewhere more persistent. When saveable state is saved, it is usually serialized to some simpler value holder (Parcelable on Android), ready to be stored by the host system. This means that what you can store in saveable state is more limited. With Parcelable on Android, you’re mostly limited to primitive values, which require writing a Saver to marshal/unmarshall.

Retained state isn’t like that. It stores the whole object in memory, so there’s no need to write a marshaller. However, the object is not persisted. If your Android app is put in the background and eventually killed, your saveable state will be saved, whilst retained state will not.

When to use Saveable vs Retained state?