# Take Compose state beyond the UI with Jetpack AppState - Speakers: Ben Sagmoe (Developer Relations Engineer), Elif Bilgin Morris (Software Engineer) - Track: Apps foundation - Video: https://www.youtube.com/watch?v=69QYEXymfVo - Page: https://developer.android.com/events/dev-summit/talks/take-compose-state-beyond-the-ui-with-jetpack-appstate ## About the talk Compose’s snapshot runtime, automatic dependency tracking, and atomic updates don’t have to stop at the UI layer. Jetpack AppState lets you move Compose state upstream—streamlining what reaches your screens by hoisting and deriving state with the snapshot system before it ever hits the UI. In this session, you’ll learn about AppState’s two foundational libraries. We’ll dive into StateStore for hoisting state, and Transform for using composition to derive state with plain Kotlin control flow—eliminating complex Flow combine chains and keeping your UI effortlessly in sync. ## Summary - Compose Runtime works as a general-purpose state management engine without Compose UI. Its snapshot system tracks reads dynamically and keeps concurrent updates consistent across properties. - Jetpack AppState Transform computes and exposes Compose State from headless composable blocks. It replaces complex Flow operator chains like `combine` while supporting early returns and nested state derivations. - Transform uses an eager headless frame clock to run transformations off the UI thread. The clock parks whenever watched states remain unchanged, avoiding wasted work. - Jetpack AppState StateStore shares Compose State across distinct components like activities and background services. Integrating it with DataStore lets individual keys survive process death without custom boilerplate. ## Resources - [Molecule library on GitHub](https://github.com/cashapp/molecule) - [AppState documentation](https://goo.gle/appstate-docs) - [AppState Recipes repository on GitHub](https://www.github.com/android/appstate-recipes) ## Topics jetpack-compose, jetpack-appstate, state-management, android-architecture, compose-runtime ## Transcript ### Introduction [00:06](https://www.youtube.com/watch?v=69QYEXymfVo&t=6s) Hello, and welcome to Take Compose State Beyond the UI with Jetpack AppState. [00:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=10s) My name is Ben, and I am a developer relations engineer working on that Jetpack architecture libraries. [00:15](https://www.youtube.com/watch?v=69QYEXymfVo&t=15s) And I'm Elif, and I'm one of the engineers who work on the app architecture libraries. [00:19](https://www.youtube.com/watch?v=69QYEXymfVo&t=19s) And today, we're here to talk to you about a future where Compose goes beyond the UI and into the data and domain layers of applications. [00:27](https://www.youtube.com/watch?v=69QYEXymfVo&t=27s) So, as I'm sure you're all well aware by now, Compose is the recommended toolkit for UI development on Android. [00:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=33s) From tat-- phones and tablets to widgets, watches, TVs, and XR devices, Compose powers it all. [00:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=39s) But as your app grows across these UI surfaces and background components, managing the state behind them gets harder, especially around two common pain points. [00:50](https://www.youtube.com/watch?v=69QYEXymfVo&t=50s) The first is deriving complex state. [00:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=52s) Weaving together multiple data streams, local UI inputs, and ambient state, like network connectivity, that often turns into a tangle of operators like combine, flatMapLatest, and distinctUntilChanged. [01:04](https://www.youtube.com/watch?v=69QYEXymfVo&t=64s) And then the second is sharing state across components: keeping an activity, service, app widget, and more in sync, and making sure that that state can survive proc-ground-- process death when needed. [01:16](https://www.youtube.com/watch?v=69QYEXymfVo&t=76s) When we stepped back to look at these challenges, we came to look at how Compose itself is built. [01:21](https://www.youtube.com/watch?v=69QYEXymfVo&t=81s) Most people think of Compose as a UI toolkit, but it's actually two distinct layers: Compose UI on top and the Compose Runtime underneath. [01:28](https://www.youtube.com/watch?v=69QYEXymfVo&t=88s) While Compose UI handles layout, drawing, and input, the Compose Runtime is a general purpose engine for state management and recomposition. [01:36](https://www.youtube.com/watch?v=69QYEXymfVo&t=96s) It has no dependency on the UI. [01:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=99s) And that's the realization that led us to build Jetpack AppState. [01:43](https://www.youtube.com/watch?v=69QYEXymfVo&t=103s) It's a family of libraries focused on making the most of Compose state beyond the UI. ### What is Compose State? [01:49](https://www.youtube.com/watch?v=69QYEXymfVo&t=109s) But before we dive into AppState, let's make sure we're on the same page when we talk about what we mean by Compose state. [01:56](https://www.youtube.com/watch?v=69QYEXymfVo&t=116s) And the answer is `androidx.compose.runtime.State`. ```kotlin /** * A value holder where reads to the [value] property during the execution of a [Composable] * function, the current [RecomposeScope] will be subscribed to changes of that value. */ @Stable public interface State { public val value: T } ``` [02:00](https://www.youtube.com/watch?v=69QYEXymfVo&t=120s) And the interface you're probably more familiar with, `MutableState`. ```kotlin /** * A mutable value holder where reads to the [value] property during the execution of a [Composable] * function, the current [RecomposeScope] will be subscribed to changes of that value. When the * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s * will be scheduled. If [value] is written to with the same value, no recompositions will be * scheduled. */ @Stable public interface MutableState : State { override var value: T public operator fun component1(): T public operator fun component2(): (T) -> Unit } ``` [02:04](https://www.youtube.com/watch?v=69QYEXymfVo&t=124s) Fundamentally, these are just bare-bones containers. [02:07](https://www.youtube.com/watch?v=69QYEXymfVo&t=127s) What makes them actually work is the Compose snapshot system. [02:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=130s) So when we say Compose state, what we really mean is usually Snapshot State. [02:16](https://www.youtube.com/watch?v=69QYEXymfVo&t=136s) So, when you call `mutableStateOf`, you're making a container that routes reads and writes through the snapshot system. [02:23](https://www.youtube.com/watch?v=69QYEXymfVo&t=143s) And whenever your code runs inside of a snapshot observer, like a composition, you get two properties that a stream like a Flow doesn't give you. ```kotlin @Composable fun Counter() { val count = remember { mutableStateOf(0) } } ``` [02:32](https://www.youtube.com/watch?v=69QYEXymfVo&t=152s) The first is automatic and dynamic read tracking. ```kotlin @Composable fun Counter() { val count = remember { mutableStateOf(0) } Column { Text("Count: ${count.value}") AddButton(onClick = { count.value++ }) } } ``` [02:35](https://www.youtube.com/watch?v=69QYEXymfVo&t=155s) So, automatic means that there's no subscribing or collecting. [02:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=159s) Just reading `.value` is the subscription. [02:42](https://www.youtube.com/watch?v=69QYEXymfVo&t=162s) So, if we look at Counter here, reading `count.value` directly inside the composable reports to the that read to the composition, and it registers Counter in its list of readers. [02:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=172s) And now look at `AddButton`. ```kotlin @Composable fun Counter() { val count = remember { mutableStateOf(0) } Column { Text("Count: ${count.value}") AddButton(onClick = { count.value++ }) } } @Composable fun AddButton(onClick: () -> Unit) { Button(onClick = onClick) { Text("Add") } } ``` [02:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=173s) It only writes to count on the click. [02:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=175s) So, it isn't a reader. [02:57](https://www.youtube.com/watch?v=69QYEXymfVo&t=177s) When a user clicks `AddButton` and modifies count, the snapshot system tracks that write, which lets the composition know to invalidate Counter, but not `AddButton`. [03:08](https://www.youtube.com/watch?v=69QYEXymfVo&t=188s) And dynamic means that read tracking happens at runtime based on which code paths actually run. ```kotlin @Composable fun Counter() { val isVisible = remember { mutableStateOf(true) } val count = remember { mutableStateOf(0) } Column { if (isVisible.value) { Text("Count: ${count.value}") } else { Text("Hidden") } AddButton(onClick = { count.value++ }) } } ``` [03:13](https://www.youtube.com/watch?v=69QYEXymfVo&t=193s) So here, we're guarding the counter text behind an `isVisible` flag. [03:17](https://www.youtube.com/watch?v=69QYEXymfVo&t=197s) When `isVisible` is true, both `isVisible` and count are read. [03:21](https://www.youtube.com/watch?v=69QYEXymfVo&t=201s) So, Counter is registered as a reader of both of them. [03:26](https://www.youtube.com/watch?v=69QYEXymfVo&t=206s) And when `isVisible` is false, if the it-- the if branch doesn't run, count isn't read, so Counter is only registered as a reader of `isVisible`, not count. ```kotlin @Composable fun Counter() { val isVisible = remember { mutableStateOf(false) } val count = remember { mutableStateOf(0) } Column { if (isVisible.value) { Text("Count: ${count.value}") } else { Text("Hidden") } AddButton(onClick = { count.value++ }) } } ``` [03:36](https://www.youtube.com/watch?v=69QYEXymfVo&t=216s) So, when `AddButton` writes to count, there's nothing left to invalidate. [03:41](https://www.youtube.com/watch?v=69QYEXymfVo&t=221s) And now, the second property is not having to choose between consistency and granularity. [03:46](https://www.youtube.com/watch?v=69QYEXymfVo&t=226s) Consistency means that when you update multiple values in a snapshot, those changes stay isolated while your code runs, even if another thread is writing to them at the same time. [03:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=235s) And those updates become visible all at once when the snapshot finishes. [04:01](https://www.youtube.com/watch?v=69QYEXymfVo&t=241s) Granularity means that separate properties can be dec-- declared and tracked individually, especially when they're unrelated. [04:07](https://www.youtube.com/watch?v=69QYEXymfVo&t=247s) So, updating one property only invalidates the readers that actually depend on it. [04:13](https://www.youtube.com/watch?v=69QYEXymfVo&t=253s) Let's take a look at this in some real code. [04:15](https://www.youtube.com/watch?v=69QYEXymfVo&t=255s) For example, we're going to look at a game that has three pieces of state: a level name, time survived, and score. ```kotlin val levelName: String = "Level 1 - Stay Alive!" val timeSurvived: Int = 0 val score: Double = 0.0 ``` [04:23](https://www.youtube.com/watch?v=69QYEXymfVo&t=263s) So to start, let's look at a con-- a Flow implementation that chooses consistency at the expense of granularity by bundling everything into a `GameUiState` class. ```kotlin data class GameUiState( val levelName: String = "Level 1 - Stay Alive!", val timeSurvived: Int = 0, val score: Double = 0.0 ) ``` [04:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=273s) That means that every time `updateScore` runs, you copy the entire `GameUiState` so that both `timeSurvived` and score are able to update together in sync. ```kotlin data class GameUiState( val levelName: String = "Level 1 - Stay Alive!", val timeSurvived: Int = 0, val score: Double = 0.0 ) class GameViewModel : ViewModel() { val uiState = MutableStateFlow(GameUiState()) fun updateScore() = uiState.update { it.copy( timeSurvived = it.timeSurvived + 1, score = it.score + 10 + .01 * it.timeSurvived ) } } ``` [04:42](https://www.youtube.com/watch?v=69QYEXymfVo&t=282s) In the UI, `GameScreen` collects the UI state and reads from it to pass the properties that are relevant down to Header and `ScoreBadge`. ```kotlin data class GameUiState( val levelName: String = "Level 1 - Stay Alive!", val timeSurvived: Int = 0, val score: Double = 0.0 ) class GameViewModel : ViewModel() { val uiState = MutableStateFlow(GameUiState()) fun updateScore() = uiState.update { it.copy( timeSurvived = it.timeSurvived + 1, score = it.score + 10 + .01 * it.timeSurvived ) } } @Composable fun GameScreen(viewModel: GameViewModel = viewModel()) { val state by viewModel.uiState.collectAsStateWithLifecycle() Column { Header(levelName = state.levelName) ScoreBadge( timeSurvived = state.timeSurvived, score = state.score ) } } ``` [04:50](https://www.youtube.com/watch?v=69QYEXymfVo&t=290s) Because `GameScreen` reads every state, every call to `updateScore` invalidates `GameScreen` even though the header's data never changed. [04:57](https://www.youtube.com/watch?v=69QYEXymfVo&t=297s) Now, you could split every property into its own Flow to get granularity, but then you would lose consistency because another thread could update a value at while the other update block is running. [05:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=309s) Now let's consider an alternate state-based implementation. ```kotlin @Stable class GameUiState { var levelName by mutableStateOf("Level 1 - Stay Alive!") var timeSurvived by mutableStateOf(0) var score by mutableStateOf(0.0) } ``` [05:13](https://www.youtube.com/watch?v=69QYEXymfVo&t=313s) In this one, we use a `GameUiState` again. [05:16](https://www.youtube.com/watch?v=69QYEXymfVo&t=316s) But in this one, it's a little bit different because each property delegates to `mutableStateOf`. [05:21](https://www.youtube.com/watch?v=69QYEXymfVo&t=321s) Here, `updateScore` wraps its writes in `withMutableSnapshot`, and this is what gives us consistency across, writes. ```kotlin @Stable class GameUiState { var levelName by mutableStateOf("Level 1 - Stay Alive!") var timeSurvived by mutableStateOf(0) var score by mutableStateOf(0.0) } class GameViewModel : ViewModel() { val uiState = GameUiState() fun updateScore() = Snapshot.withMutableSnapshot { uiState.timeSurvived += 1 uiState.score += 10 + .01 * uiState.timeSurvived } } ``` [05:30](https://www.youtube.com/watch?v=69QYEXymfVo&t=330s) And then in the UI, `GameScreen` passes State straight through to Header and `ScoreBadge`. ```kotlin @Stable class GameUiState { var levelName by mutableStateOf("Level 1 - Stay Alive!") var timeSurvived by mutableStateOf(0) var score by mutableStateOf(0.0) } class GameViewModel : ViewModel() { val uiState = GameUiState() fun updateScore() = Snapshot.withMutableSnapshot { uiState.timeSurvived += 1 uiState.score += 10 + .01 * uiState.timeSurvived } } @Composable fun GameScreen(viewModel: GameViewModel = viewModel()) { val state = viewModel.uiState Column { Header(state = state) ScoreBadge(state = state) } } ``` [05:34](https://www.youtube.com/watch?v=69QYEXymfVo&t=334s) It doesn't read any of the properties, so when `updateScore` runs, `GameScreen` isn't a reader. [05:40](https://www.youtube.com/watch?v=69QYEXymfVo&t=340s) Only the actual component, in this case, `ScoreBadge`, is invalidated. [05:46](https://www.youtube.com/watch?v=69QYEXymfVo&t=346s) And lastly, notice that none of this is tied to the UI. [05:49](https://www.youtube.com/watch?v=69QYEXymfVo&t=349s) The UI just happens to be one place where these properties really shine. [05:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=353s) And with that, thanks for coming to our talk. [05:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=355s) Okay, jokes aside, I'll hand it over to Elif to take us through the first library, Transform. ### Introducing Transform [06:01](https://www.youtube.com/watch?v=69QYEXymfVo&t=361s) Okay. [06:02](https://www.youtube.com/watch?v=69QYEXymfVo&t=362s) Thanks, Ben. [06:03](https://www.youtube.com/watch?v=69QYEXymfVo&t=363s) So, as Ben just noted, Compose snapshot state offers us automatic and dynamic read tracking, consistency, and granularity, which brings me to the question, what if we could have our data flows also driven by a Compose State Snapshot-- a Snapshot State? [06:20](https://www.youtube.com/watch?v=69QYEXymfVo&t=380s) And if state is our data model, then how do we actually compute and transform a state for all of its consumers? [06:27](https://www.youtube.com/watch?v=69QYEXymfVo&t=387s) This is where the new Jetpack AppState Transform library comes in. [06:32](https://www.youtube.com/watch?v=69QYEXymfVo&t=392s) Before we dive into the library itself, we need to talk about how the library came to be. [06:37](https://www.youtube.com/watch?v=69QYEXymfVo&t=397s) So, the idea of taking advantage of the Compose engine, which is the runtime and the compiler here, to produce state values in a Headless composition, is not a new one. [06:47](https://www.youtube.com/watch?v=69QYEXymfVo&t=407s) Many of you might already be using or might already be familiar with the Molecule library. [06:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=412s) And by Headless here, we mean there's no UI. [06:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=415s) So Molecule was pioneering in demonstrating the power of using Compose Runtime, and it did so by showing us in action that a composable function doesn't have to emit UI nodes, and it can actually be used to write composable function that return a pure data model. [07:13](https://www.youtube.com/watch?v=69QYEXymfVo&t=433s) In Molecule's case, this was a Kotlin Flow or a `StateFlow`. [07:18](https://www.youtube.com/watch?v=69QYEXymfVo&t=438s) This meant that instead of chaining react-- reactive operators like combine, you could write ordinary Kotlin. [07:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=444s) But instead of dealing with flows, what if your data model was State, since your UI is already in Compose? [07:32](https://www.youtube.com/watch?v=69QYEXymfVo&t=452s) With Transform, your data model is the State. [07:35](https://www.youtube.com/watch?v=69QYEXymfVo&t=455s) Let's take a look at the actual API. ```kotlin public fun transform( initialValue: R, scope: CoroutineScope, onUpdate: @Composable (R) -> R, ): State ``` [07:38](https://www.youtube.com/watch?v=69QYEXymfVo&t=458s) This is the whole API. [07:40](https://www.youtube.com/watch?v=69QYEXymfVo&t=460s) It's very simple, one function. [07:42](https://www.youtube.com/watch?v=69QYEXymfVo&t=462s) It returns a plain observable State, not a Flow, not a `StateFlow`. [07:46](https://www.youtube.com/watch?v=69QYEXymfVo&t=466s) And whoever consumes it doesn't know or care that a Transform produced it. [07:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=473s) What's really interesting here is this block. [07:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=475s) It's Composable. [07:57](https://www.youtube.com/watch?v=69QYEXymfVo&t=477s) This means that everything you already know works inside of it. [08:00](https://www.youtube.com/watch?v=69QYEXymfVo&t=480s) Remember `LaunchedEffect`, `collectAsState`, early returns, plain ifs, anything you can read from inside of a Composable, you can derive from in a Transform. [08:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=489s) We should note that all of the APIs we're seeing today in this presentation are all in alpha, and they're subject to change, but regardless, let's take it look take a look at an example. [08:19](https://www.youtube.com/watch?v=69QYEXymfVo&t=499s) So, let's take a very simple, small focus timer app. [08:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=504s) You pick a task, you set a 25-minute timer, the clock ticks down. [08:28](https://www.youtube.com/watch?v=69QYEXymfVo&t=508s) Every time you're creating a single screen, even when you're creating a simple single screen like this, you probably will need to combine and manage multiple independent data streams to assemble your state under the hood. [08:41](https://www.youtube.com/watch?v=69QYEXymfVo&t=521s) For our focus screen, let's say we have two: the focus session itself, which is going to be what phase we're in, so paused or not, how many seconds are left, and all of this is going to be coming from a timer engine as a `StateFlow` that's going to tick let's say every one second. [08:58](https://www.youtube.com/watch?v=69QYEXymfVo&t=538s) The other data stream is going to be the current task that the user's working on that's going to come in from a Room database as a Flow. [09:05](https://www.youtube.com/watch?v=69QYEXymfVo&t=545s) So, two data streams and one screen state at the end. [09:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=550s) The `ViewModel` is going to be where they get put together, and its output, which we're going to call the summary, is going to be the piece of code that we're going to write next. [09:20](https://www.youtube.com/watch?v=69QYEXymfVo&t=560s) Traditionally, you would, this would mean that in your `ViewModel`, you will have to combine these flows using chained operators. ```kotlin class FocusScreenViewModel( focusManager: FocusSessionManager, taskRepository: TaskRepository ) : ViewModel() { val summary: StateFlow = combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks -> if (session.phase == FocusPhase.Idle) { FocusSummary.Idle } else { val task = tasks.find { it.id == session.activeTaskId } FocusSummary.Active(...) } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = FocusSummary.Idle, ) } ``` [09:26](https://www.youtube.com/watch?v=69QYEXymfVo&t=566s) So, combine the two zip the two flows together into streams together, the branching inside the lambda, so if else right there, and then a state in at the end. [09:37](https://www.youtube.com/watch?v=69QYEXymfVo&t=577s) With Transform, we can drop this entire boilerplate code. ```kotlin class FocusScreenViewModel( focusManager: FocusSessionManager, taskRepository: TaskRepository ) : ViewModel() { val summary: State = transform( initialValue = FocusSummary.Idle, scope = viewModelScope, container = handle.getOrCreateContainer("transform") ) { val session by focusManager.sessionFlow.collectAsState() if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle val tasks by taskRepository.tasksFlow.collectAsState(emptyList()) val task = tasks.find { it.id == session.activeTaskId } FocusSummary.Active(...) ``` [09:41](https://www.youtube.com/watch?v=69QYEXymfVo&t=581s) This call looks kind of familiar because we have an initial value in a scope being passed in, but there's nothing upstream to chain, the derivation is the block itself. [09:50](https://www.youtube.com/watch?v=69QYEXymfVo&t=590s) And unlike `stateIn` when you would probably need to configure a `WhileSubscribed` time out to pause and resume upstream streams, a Transform runs continuously within the lifetime of that scope. [10:01](https://www.youtube.com/watch?v=69QYEXymfVo&t=601s) Again, our sources are flows, and flows are welcome. ```kotlin class FocusScreenViewModel( focusManager: FocusSessionManager, taskRepository: TaskRepository ) : ViewModel() { val summary: State = transform( initialValue = FocusSummary.Idle, scope = viewModelScope, container = handle.getOrCreateContainer("transform") ) { val session by focusManager.sessionFlow.collectAsState() if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle val tasks by taskRepository.tasksFlow.collectAsState(emptyList()) val task = tasks.find { it.id == session.activeTaskId } FocusSummary.Active(...) } } ``` [10:04](https://www.youtube.com/watch?v=69QYEXymfVo&t=604s) We're going to bridge each one with `collectAsState`, and the session is a `StateFlow`, so we don't need an initial value, but the Room Flow is cold, so we give it one. [10:12](https://www.youtube.com/watch?v=69QYEXymfVo&t=612s) And that's the whole subscription story. [10:14](https://www.youtube.com/watch?v=69QYEXymfVo&t=614s) The read is what subscribes us. [10:16](https://www.youtube.com/watch?v=69QYEXymfVo&t=616s) It's the automatic read tracking that Ben was talking about earlier in the example we saw, but in a `ViewModel`. [10:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=624s) We can even introduce early returns like this here when we're in an idle state. [10:29](https://www.youtube.com/watch?v=69QYEXymfVo&t=629s) Let's say, we have no new tasks, we won't even process the rest. [10:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=633s) And again, this is the dynamic read tracking paying off again. [10:36](https://www.youtube.com/watch?v=69QYEXymfVo&t=636s) While we're idle, the Room Flow is not collected at all. [10:40](https://www.youtube.com/watch?v=69QYEXymfVo&t=640s) We then finally emit the summary. [10:42](https://www.youtube.com/watch?v=69QYEXymfVo&t=642s) So, if the user pauses the task or the timer ticks, everything automat-- automatically gets updated. ### Nested transforms [10:48](https://www.youtube.com/watch?v=69QYEXymfVo&t=648s) So, things get a bit more complicated when we step outside of the single `ViewModel` example. [10:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=653s) Let's say, this same session needs to show up on two more additional surfaces, a mini-timer bar that lives at the bottom of the app's screen on every screen, and a home-screen widget. [11:07](https://www.youtube.com/watch?v=69QYEXymfVo&t=667s) Both of these will need the same derived information about the session summary, which phase, paused or not, what-- how much time left in the timer. [11:16](https://www.youtube.com/watch?v=69QYEXymfVo&t=676s) And this is going to mean that if every single surface were to assemble its own copy, that's the same derivation three times and three different places that all have to be in agreement with each other. [11:27](https://www.youtube.com/watch?v=69QYEXymfVo&t=687s) So, what we'll need to do instead is derive the summary once in the application scope and then everything downstream will consume it. [11:36](https://www.youtube.com/watch?v=69QYEXymfVo&t=696s) So, how do we actually go about implementing something like this? [11:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=699s) Where our implementation is probably going to need three pieces. ```kotlin class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) { ... } class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() { ... } class FocusWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { ... } } } ``` [11:43](https://www.youtube.com/watch?v=69QYEXymfVo&t=703s) So, a session manager in the application scope that's going to be where the summary gets derived, a mini-timer's `ViewModel`, and a Glance Widget, both of which is going to consume the summary derived in this focus session manager. [11:57](https://www.youtube.com/watch?v=69QYEXymfVo&t=717s) Let's build each one like the traditional way and then with Transform. [12:03](https://www.youtube.com/watch?v=69QYEXymfVo&t=723s) So, traditionally, again, we would start by deriving the summary once in the application scope as a `StateFlow`. ```kotlin class FocusSessionManager( taskRepository: TaskRepository, scope: CoroutineScope ) { val sessionFlow: StateFlow = ... val summary: StateFlow = combine( sessionFlow, taskRepository.tasksFlow ) { session, tasks -> if (session.phase == FocusPhase.Idle) { FocusSummary.Idle } else { val task = tasks.find { it.id == session.activeTaskId } FocusSummary.Active(...) } }.stateIn( scope = appScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = FocusSummary.Idle ) } ``` [12:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=730s) It's the same combine and `stateIn` calls that we saw earlier in the `ViewModel` example, but just moved up a level. [12:17](https://www.youtube.com/watch?v=69QYEXymfVo&t=737s) With Transform, we can also reuse the same declaration from the `ViewModel` we saw earlier. ```kotlin class FocusSessionManager( taskRepository: TaskRepository, scope: CoroutineScope ) { val sessionFlow: StateFlow = ... val summary: State = transform( initialValue = FocusSummary.Idle, scope = scope, container = handle.getOrCreateContainer("transform") ) { val session by sessionFlow.collectAsState() if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle val tasks by taskRepository.tasksFlow .collectAsState(emptyList()) val task = tasks.find { it.id == session.activeTaskId } FocusSummary.Active(...) } } ``` [12:22](https://www.youtube.com/watch?v=69QYEXymfVo&t=742s) The only thing that's changed is, again, it now lives in the application scope. [12:28](https://www.youtube.com/watch?v=69QYEXymfVo&t=748s) Next up, we have the mini-timer's `ViewModel`. ```kotlin class MiniTimerViewModel( focusManager: FocusSessionManager ) : ViewModel() { val isExpanded = MutableStateFlow(false) val uiState: StateFlow = combine( focusManager.summary, isExpanded ) { summary, expanded -> MiniTimerUiState( summary = summary, showControls = expanded && summary is FocusSummary.Active ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = MiniTimerUiState() ) fun toggleExpanded() { isExpanded.update { !it } } } ``` [12:31](https://www.youtube.com/watch?v=69QYEXymfVo&t=751s) Let's say, we want to give ourselves a little bit of a challenge here, too. [12:34](https://www.youtube.com/watch?v=69QYEXymfVo&t=754s) Let's say that in the mini-timer, in addition to the two data streams that we had already, the `StateFlow` and the Room Flow, we also want to track whether the timer bar is expanded or not. [12:44](https://www.youtube.com/watch?v=69QYEXymfVo&t=764s) And we use a `MutableStateFlow` for that. [12:48](https://www.youtube.com/watch?v=69QYEXymfVo&t=768s) To do this, we're going to have to use another combine to track the summary flow and the `isExpanded` `StateFlow`. [12:54](https://www.youtube.com/watch?v=69QYEXymfVo&t=774s) And we get a second state in. [12:56](https://www.youtube.com/watch?v=69QYEXymfVo&t=776s) With Transform, we don't have to use any of these chained operators again to achieve the syncing between data streams. ```kotlin class MiniTimerViewModel( focusManager: FocusSessionManager ) : ViewModel() { val isExpanded = MutableStateFlow(false) val uiState: State = transform( initialValue = MiniTimerUiState(), scope = viewModelScope, container = handle.getOrCreateContainer("transform") ) { val summary by focusManager.summary val expanded by isExpanded.collectAsState() MiniTimerUiState( summary = summary, showControls = expanded && summary is FocusSummary.Active ) } fun toggleExpanded() { isExpanded.update { !it } } } ``` [13:02](https://www.youtube.com/watch?v=69QYEXymfVo&t=782s) What we can do, and this is really cool, is that we can do nested Transforms. [13:07](https://www.youtube.com/watch?v=69QYEXymfVo&t=787s) So, here the `ViewModel` reads the summary, which is the output of the upstream Transform, just like any other state, and then it collects `isExpanded` with `collectAsState`. [13:17](https://www.youtube.com/watch?v=69QYEXymfVo&t=797s) So, we can mix an upstream state and a downstream flow in the `StateFlow` right in the same block. [13:25](https://www.youtube.com/watch?v=69QYEXymfVo&t=805s) And remember granularity. [13:27](https://www.youtube.com/watch?v=69QYEXymfVo&t=807s) So, if you were to toggle `isExpanded`, it's only going to rerun this block only. [13:32](https://www.youtube.com/watch?v=69QYEXymfVo&t=812s) The app-level block and the widget don't even have to notice. [13:37](https://www.youtube.com/watch?v=69QYEXymfVo&t=817s) Lastly, we have the widget. ```kotlin class FocusWidget : GlanceAppWidget() { override suspend fun provideGlance( context: Context, id: GlanceId ) { val focusManager = ... provideContent { val summary by focusManager.summary .collectAsState() GlanceTheme { FocusWidgetContent(summary) } } } } ``` [13:40](https://www.youtube.com/watch?v=69QYEXymfVo&t=820s) Since Glance is Compose, we can collect the `StateFlow` with `collectAsState` inside a `provideContent` like any other Composable. [13:47](https://www.youtube.com/watch?v=69QYEXymfVo&t=827s) With Transform, this is even simpler. ```kotlin class FocusWidget : GlanceAppWidget() { override suspend fun provideGlance( context: Context, id: GlanceId ) { val focusManager = ... provideContent { val summary by focusManager.summary GlanceTheme { FocusWidgetContent(summary) } } } } ``` [13:49](https://www.youtube.com/watch?v=69QYEXymfVo&t=829s) Our focus manager is providing us with a Compose State, so there's nothing that we need to convert here. [13:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=835s) And ultimately, the mini-timer bar and the home-screen widget can never disagree because they're reading the same state. [14:02](https://www.youtube.com/watch?v=69QYEXymfVo&t=842s) And not only that, but any change made to that state will be reflected to all of the consumers of that state. [14:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=849s) What does that mean? [14:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=850s) So, if you were to go and hit pause on the widget here, then it's going to go through the manager into the session stream, back out through the summary, and all three of these screens are going to be updated in sync. [14:26](https://www.youtube.com/watch?v=69QYEXymfVo&t=866s) So, now that we've looked at how to use Transform a little bit, let's take a deeper dive under the hood. [14:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=873s) The Transform API that we saw earlier is actually a helper function used to create a basic managed Transform. ```kotlin public fun transform( initialValue: R, scope: CoroutineScope, onUpdate: @Composable (R) -> R, ): State { val transformScope = TransformScope() val transform = transformScope .launchTransform(initialValue, onUpdate) scope.launch( dispatcher, start = CoroutineStart.UNDISPATCHED ) { try { transformScope.run() } finally { transform.dispose() } } return transform } ``` [14:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=879s) In reality, it actually uses a `TransformScope` and a `launchTransform` extension function, which launches a Composable transformation in this `TransformScope` and returns a disposable state. [14:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=892s) When you read it top to bottom, you're going to see that it does everything for you. [14:56](https://www.youtube.com/watch?v=69QYEXymfVo&t=896s) It creates the `TransformScope` and launches your Transform into it, which is going to be the state that you get back. [15:01](https://www.youtube.com/watch?v=69QYEXymfVo&t=901s) And then it also launches run in the `TransformScope` on the dispatcher that you provided, so that the scope's lifetime and the thread belong to you. [15:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=910s) And then the finally disposes of your transformation when the scope ends. [15:15](https://www.youtube.com/watch?v=69QYEXymfVo&t=915s) This is the whole life-cycle story of Transform. [15:18](https://www.youtube.com/watch?v=69QYEXymfVo&t=918s) So, let's take a look at the `TransformScope` interface here really quickly. ```kotlin public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope public abstract class TransformScope { public fun createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState public abstract suspend fun run() } public fun TransformScope.launchTransform( initialState: T, onUpdate: @Composable (T) -> T, ): DisposableState public interface DisposableState : State { public fun dispose() } ``` [15:25](https://www.youtube.com/watch?v=69QYEXymfVo&t=925s) We can see that it has a `createTransform` function, which is going to create a Transform attached to this `TransformScope`. [15:31](https://www.youtube.com/watch?v=69QYEXymfVo&t=931s) And it also has a suspending run function, which starts the `TransformScope` and begins the process of updating the present state transformations. [15:40](https://www.youtube.com/watch?v=69QYEXymfVo&t=940s) What does that mean? [15:41](https://www.youtube.com/watch?v=69QYEXymfVo&t=941s) So, run actually sets up the Recomposer and the composition. [15:45](https://www.youtube.com/watch?v=69QYEXymfVo&t=945s) And every Transform in the scope lives inside that composition and has its very own recompose scope. [15:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=953s) This is why we want to share scope and why we have `TransformScope`. [15:56](https://www.youtube.com/watch?v=69QYEXymfVo&t=956s) So that whenever a state changes, only the Transforms that read it are rerun. [16:02](https://www.youtube.com/watch?v=69QYEXymfVo&t=962s) As we noted earlier, run is a suspend function. ```kotlin override suspend fun run() { val context = currentCoroutineContext() val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock() ... val recomposer = Recomposer(context) val composition = Composition(UnitApplier, recomposer) try { withContext(clock) { if (clock is HeadlessTransformClock) { launch(start = CoroutineStart.UNDISPATCHED) { clock.runClock() } } ... } } finally { // dispose of composition, cancel the clock, close the recomposer } } ``` [16:06](https://www.youtube.com/watch?v=69QYEXymfVo&t=966s) So, it runs on whatever coroutine that you call it from until that coroutine is canceled. [16:11](https://www.youtube.com/watch?v=69QYEXymfVo&t=971s) Now, you might be asking or wondering, a Recomposer needs a frame clock to tell it when it needs to produce a frame. [16:19](https://www.youtube.com/watch?v=69QYEXymfVo&t=979s) In Compose UI, that's the display, but as we mentioned earlier, we're in a headless environment, so there is no UI. [16:25](https://www.youtube.com/watch?v=69QYEXymfVo&t=985s) So, unless you provide us with a clock, we're going to create our own. [16:30](https://www.youtube.com/watch?v=69QYEXymfVo&t=990s) By decoupling recomposition from the display frame clock, Transform runs all of its transformations and state derivations off the UI thread. [16:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=999s) To allow this, we built a dedicated `HeadlessTransformClock`. [16:42](https://www.youtube.com/watch?v=69QYEXymfVo&t=1002s) Transform frame clock. [16:45](https://www.youtube.com/watch?v=69QYEXymfVo&t=1005s) Since a widget, a service, or another Transform downstream might be waiting on a value from a Transform, our clock is eager. [16:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=1012s) This means that the moment any state a Transform is consuming changes, a new frame is produced immediately. [16:59](https://www.youtube.com/watch?v=69QYEXymfVo&t=1019s) It's a small lock-free state machine, so requesting a frame is a simple compare-and-set operation. [17:05](https://www.youtube.com/watch?v=69QYEXymfVo&t=1025s) And when there's nothing that has changed in the state, the clock parks. [17:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=1029s) So, a Transform that's consuming a state that hasn't been changed at all costs you nothing. [17:15](https://www.youtube.com/watch?v=69QYEXymfVo&t=1035s) Before I hand it off back to Ben, we should also note that Transform now supports rememberSaveable to ensure that your data is saved across configuration changes. [17:23](https://www.youtube.com/watch?v=69QYEXymfVo&t=1043s) It does so through the new SavedStateContainer API, which is now the building block on which Compose's SaveableStateRegistry, `SavedStateHandle`, and SavedStateRegistry are all built on. [17:35](https://www.youtube.com/watch?v=69QYEXymfVo&t=1055s) You can turn any of these into a SavedStateContainer or create child containers and pass them into your Transform to ensure that your data is saved. [17:43](https://www.youtube.com/watch?v=69QYEXymfVo&t=1063s) Finally, a reminder again that all the APIs that we're talking about today are all in alpha, so they are subject to change. [17:49](https://www.youtube.com/watch?v=69QYEXymfVo&t=1069s) With that, handing it off back to Ben. ### Compose State outside of the UI [17:50](https://www.youtube.com/watch?v=69QYEXymfVo&t=1070s) All right. [17:50](https://www.youtube.com/watch?v=69QYEXymfVo&t=1070s) All righty, thanks, Elif. [17:56](https://www.youtube.com/watch?v=69QYEXymfVo&t=1076s) So, now that we've seen how Compose state is powerful and how Transform lets us derive it outside of the UI, one thing you might have noticed in the example so far is our inputs were Flows that we bridged using `collectAsState`. [18:08](https://www.youtube.com/watch?v=69QYEXymfVo&t=1088s) But is that always necessary? [18:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=1090s) Why not use Compose state directly in our state holders? [18:13](https://www.youtube.com/watch?v=69QYEXymfVo&t=1093s) So, to answer that question, I find it helpful to think about state within three broad categories. [18:18](https://www.youtube.com/watch?v=69QYEXymfVo&t=1098s) The first is live state that's only ever stored in memory. [18:21](https://www.youtube.com/watch?v=69QYEXymfVo&t=1101s) Think things like network or Bluetooth connection status. [18:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=1104s) You wouldn't want to use a persisted value. [18:26](https://www.youtube.com/watch?v=69QYEXymfVo&t=1106s) And then, the second is transient state that should survive process death, but doesn't need to be stored indefinitely. [18:31](https://www.youtube.com/watch?v=69QYEXymfVo&t=1111s) Think things like UI state, such as whether a card is expanded or not, and session state, like your navigation back stack or a form draft. [18:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=1119s) And then, the third is permanent state, which is stored indefinitely. [18:43](https://www.youtube.com/watch?v=69QYEXymfVo&t=1123s) Think things like user preferences or a media library living in a database. [18:48](https://www.youtube.com/watch?v=69QYEXymfVo&t=1128s) So, let's start with how you can use Compose state for both live and transient state. [18:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=1133s) Up until now, our architecture guidance kept Compose state in the UI layer, or at sometimes, in the ViewModels, and we told you to use Flow everywhere else. [19:02](https://www.youtube.com/watch?v=69QYEXymfVo&t=1142s) But why was that? [19:03](https://www.youtube.com/watch?v=69QYEXymfVo&t=1143s) And it's because as soon as you needed to combine a state property with a Flow, you had to convert it back using snapshotFlow. [19:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=1149s) But with Transform, that conversion isn't necessary. [19:12](https://www.youtube.com/watch?v=69QYEXymfVo&t=1152s) So, let's look at what this looks or what this means, starting in a `ViewModel`. ```kotlin @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, private val networkMonitor: NetworkMonitor, savedStateHandle: SavedStateHandle, ) : ViewModel() { ... } ``` [19:17](https://www.youtube.com/watch?v=69QYEXymfVo&t=1157s) For local screen inputs, like `selectedTaskIds` here, you can declare plain `mutableStateOf` property instead of `MutableStateFlow`. ```kotlin @HiltViewModel class TaskListViewModel @Inject constructor(...) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet()) private set } ``` [19:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=1164s) And because this is something that you does don't necessarily want to persist across process death, it's okay that it's not like that it's just using `mutableStateOf`. [19:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=1173s) For things that you do want to survive process death, like the search query, you can back its mutable state directly with `savedStateHandle.saveable`. ```kotlin @HiltViewModel class TaskListViewModel @Inject constructor(...) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set } ``` [19:41](https://www.youtube.com/watch?v=69QYEXymfVo&t=1181s) And then, just like `updateScore` in the game example earlier, when a method updates multiple state values at once, wrapping the writes in `withMutableSnapshot` ensures consistency. ```kotlin @HiltViewModel class TaskListViewModel @Inject constructor(...) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } } ``` [19:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=1192s) Then, to combine `searchQuery` with a Room Flow, you just read it directly inside the Transform, while collecting the Flow. ```kotlin val tasksState: State = transform( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success(tasks = tasks) } ``` [20:00](https://www.youtube.com/watch?v=69QYEXymfVo&t=1200s) So, what about live state like network connectivity? ```kotlin @Singleton class NetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) ... } ``` [20:03](https://www.youtube.com/watch?v=69QYEXymfVo&t=1203s) This usually lives in a Singleton that's because it's read across multiple screens or even by multiple app components, like services. [20:10](https://www.youtube.com/watch?v=69QYEXymfVo&t=1210s) So, but that doesn't mean you can't use Compose State to store it. [20:15](https://www.youtube.com/watch?v=69QYEXymfVo&t=1215s) In fact, it's often simpler than a flow-based version. [20:17](https://www.youtube.com/watch?v=69QYEXymfVo&t=1217s) You don't need a callback flow or anything to do the adaption, you just update dot value directly in the callbacks. ```kotlin @Singleton class NetworkMonitor @Inject constructor(...) { ... private val _isOnline = mutableStateOf(checkCurrentConnectivity()) val isOnline: State = _isOnline } ``` [20:23](https://www.youtube.com/watch?v=69QYEXymfVo&t=1223s) And, if a consumer still needs a flow, you can use `snapshotFlow` to bridge back. ```kotlin @Singleton class NetworkMonitor @Inject constructor(...) { ... private val _isOnline = mutableStateOf(checkCurrentConnectivity()) val isOnline: State = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { _isOnline.value = true } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } } ``` [20:28](https://www.youtube.com/watch?v=69QYEXymfVo&t=1228s) So, let's take a look what that looks like in the `ViewModel`. [20:32](https://www.youtube.com/watch?v=69QYEXymfVo&t=1232s) You can read `isOnline` directly in a Transform, and it'll recompose when the value changes. ```kotlin val tasksState: State = transform( initialValue = TaskListState.Loading, scope = viewModelScope, ) { if (!networkMonitor.isOnline.value) { return@transform TaskListState.Offline } val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success(tasks = tasks) } ``` [20:37](https://www.youtube.com/watch?v=69QYEXymfVo&t=1237s) And, all this is just happening directly, there's no like, application scope, or, coroutine scope that you need to provide to update the state, it's just, you know, a direct, synchronous update. [20:48](https://www.youtube.com/watch?v=69QYEXymfVo&t=1248s) So, with Transform, your `ViewModel` can combine State from Singleton managers, State in a `ViewModel`, and Flows from Repositories, all in a single Transform block. [20:58](https://www.youtube.com/watch?v=69QYEXymfVo&t=1258s) And, now let's go back to transient State. [21:00](https://www.youtube.com/watch?v=69QYEXymfVo&t=1260s) Earlier, I mentioned two types: UI state and session state. [21:04](https://www.youtube.com/watch?v=69QYEXymfVo&t=1264s) For UI state, and even session state that lives inside of a single Activity, `ViewModel` and SavedState work great. [21:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=1269s) That's exactly what they were designed for. [21:12](https://www.youtube.com/watch?v=69QYEXymfVo&t=1272s) But, when things start to break down is when that session state needs to be shared across a bunch of different UIs, and app components. [21:18](https://www.youtube.com/watch?v=69QYEXymfVo&t=1278s) Once that state moves outside of a `ViewModel`, our existing APIs and guidance leave a bit of a gap. [21:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=1284s) So, first the SavedState APIs save into an Activity's Bundle, but that's not available in other app components, like a service. [21:30](https://www.youtube.com/watch?v=69QYEXymfVo&t=1290s) Second, `mutableStateOf` in a Singleton, that only lives in memory, so it doesn't survive process death. ### Introducing StateStore [21:36](https://www.youtube.com/watch?v=69QYEXymfVo&t=1296s) And, that's the gap that Jetpack AppState, `StateStore` fills. [21:40](https://www.youtube.com/watch?v=69QYEXymfVo&t=1300s) It's a type-safe map of Compose State that can be shared across app components and backed by `DataStore` to provide, to survive process death. [21:47](https://www.youtube.com/watch?v=69QYEXymfVo&t=1307s) You could wire `DataStore` into every Singleton yourself, but that means writing save and restore boilerplate for every property. [21:53](https://www.youtube.com/watch?v=69QYEXymfVo&t=1313s) With the `StateStore` `DataStore` Integration, you hook up the `DataStore` once, and then you opt in each little bit of state that you want to persist. [22:00](https://www.youtube.com/watch?v=69QYEXymfVo&t=1320s) So, let's build out that in our focus timer app. [22:03](https://www.youtube.com/watch?v=69QYEXymfVo&t=1323s) We're going to start in-memory first, and then add `DataStore` persistence afterwards. [22:07](https://www.youtube.com/watch?v=69QYEXymfVo&t=1327s) To set the stage, we're modeling a `FocusSession` as a sealed interface with idle, running, and paused states. ```kotlin sealed interface FocusSession { data object Idle : FocusSession data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession } ``` [22:14](https://www.youtube.com/watch?v=69QYEXymfVo&t=1334s) And then, to store this state in `StateStore`, we defined a `StateStoreKey` with its type, `FocusSession`, and a default value, idle. ```kotlin sealed interface FocusSession { ... } object FocusSessionKey : StateStoreKey(defaultValue = FocusSession.Idle) ``` [22:23](https://www.youtube.com/watch?v=69QYEXymfVo&t=1343s) And then, we provide a Singleton `StateStore` in Hilt that we can inject into different components where we need it, such as Focus Session Manager. ```kotlin @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideStateStore(): StateStore = StateStore() } @Singleton class FocusSessionManager @Inject constructor( private val stateStore: StateStore, ) { ... } ``` [22:30](https://www.youtube.com/watch?v=69QYEXymfVo&t=1350s) So, similar to how the `ViewModel` injects `SavedStateHandle`. [22:34](https://www.youtube.com/watch?v=69QYEXymfVo&t=1354s) Okay. [22:34](https://www.youtube.com/watch?v=69QYEXymfVo&t=1354s) So, now we've got a `StateStore`, but how do we use it? [22:37](https://www.youtube.com/watch?v=69QYEXymfVo&t=1357s) Well, good thing, it's a small API. [22:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=1359s) It just has three methods, so we're going to go through them one by one. [22:42](https://www.youtube.com/watch?v=69QYEXymfVo&t=1362s) The first is `setState`, which writes a new value for a key. ```kotlin fun startFocus(taskId: String, durationMinutes: Int = 25) { val endTime = System.currentTimeMillis() + durationMinutes * 60_000L stateStore.setState( FocusSessionKey, FocusSession.Running(taskId = taskId, endTimeMillis = endTime), ) } fun endFocus() { stateStore.setState(FocusSessionKey, FocusSession.Idle) } ``` [22:46](https://www.youtube.com/watch?v=69QYEXymfVo&t=1366s) When a session starts, we set it to running, and when the session ends, we set it to idle. [22:50](https://www.youtube.com/watch?v=69QYEXymfVo&t=1370s) That's it. [22:52](https://www.youtube.com/watch?v=69QYEXymfVo&t=1372s) Second, we have, still have, `updateState`, which lets you compute a new value based off of the current value, such as calculating the time remaining when switching between running and paused. ```kotlin fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) { stateStore.updateState(FocusSessionKey) { session -> if (session !is FocusSession.Running) return@updateState session FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis) } } fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) { stateStore.updateState(FocusSessionKey) { session -> if (session !is FocusSession.Paused) return@updateState session FocusSession.Running(session.taskId, nowMillis + session.remainingMillis) } } ``` [23:03](https://www.youtube.com/watch?v=69QYEXymfVo&t=1383s) And lastly, there's `getState`, which returns a read-only State for a key, and it has the option to override the default value, if you want to change that up at a specific call site. ```kotlin @Singleton class FocusSessionManager @Inject constructor( private val stateStore: StateStore, ) { val session: State = stateStore.getState(FocusSessionKey) ... } ``` [23:12](https://www.youtube.com/watch?v=69QYEXymfVo&t=1392s) And, exposing the `FocusSession` State in this manager means that consumers never have to access `StateStore` or its keys directly. [23:18](https://www.youtube.com/watch?v=69QYEXymfVo&t=1398s) You can in-- maintain write and read encapsulation. [23:22](https://www.youtube.com/watch?v=69QYEXymfVo&t=1402s) And then, instead of consumers they just inject Focus Session Manager, which, well, they probably already have because they need to control the session, and then they can read the State. ```kotlin @HiltViewModel class MiniTimerViewModel @Inject constructor( focusSessionManager: FocusSessionManager, ) : ViewModel() { ... } @EntryPoint @InstallIn(SingletonComponent::class) interface WidgetEntryPoint { fun focusSessionManager(): FocusSessionManager } ``` [23:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=1413s) And then, those readers, they can either directly re-expose the State, or they can use Transform to make a new, updated State kind of like we saw with the Mini-Timer `ViewModel` example earlier. ```kotlin @HiltViewModel class MiniTimerViewModel @Inject constructor( focusSessionManager: FocusSessionManager, ) : ViewModel() { val session: State = focusSessionManager.session } ``` [23:45](https://www.youtube.com/watch?v=69QYEXymfVo&t=1425s) And then, the UI Composables can read that State just like any other Compose State and recompose when necessary. ```kotlin @HiltViewModel class MiniTimerViewModel @Inject constructor( focusSessionManager: FocusSessionManager, ) : ViewModel() { val session: State = focusSessionManager.session } @Composable fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) { val session by viewModel.session if (session !is FocusSession.Idle) { TimerReadout(session = session) } } ``` [23:51](https://www.youtube.com/watch?v=69QYEXymfVo&t=1431s) But so far, the `FocusSession` only ever lives in memory. [23:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=1435s) So, let's look at what it makes takes to survive process death. [23:58](https://www.youtube.com/watch?v=69QYEXymfVo&t=1438s) So, first, we have to do some one-time setup by launching the `syncToDataStore` when we create the store. ```kotlin @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideStateStore( @ApplicationContext context: Context, @ApplicationScope scope: CoroutineScope, ): StateStore = StateStore().apply { val path = context.dataStoreFile("app_state.json").absolutePath scope.launch { syncToDataStore(path = path, coroutineScope = scope) } } } ``` [24:04](https://www.youtube.com/watch?v=69QYEXymfVo&t=1444s) And then, after that, we opt in each key that we want to persist. ```kotlin @Serializable sealed interface FocusSession { @Serializable data object Idle : FocusSession @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession } object FocusSessionKey : StateStoreKey(defaultValue = FocusSession.Idle), PersistToDataStore ``` [24:09](https://www.youtube.com/watch?v=69QYEXymfVo&t=1449s) All we have to do is make sure that the type is serializable and that the key implements `PersistToDataStore` interface. [24:16](https://www.youtube.com/watch?v=69QYEXymfVo&t=1456s) And then, you might be wondering, okay, that seems easy. [24:19](https://www.youtube.com/watch?v=69QYEXymfVo&t=1459s) So, what does it actually do to persist the key? [24:22](https://www.youtube.com/watch?v=69QYEXymfVo&t=1462s) So, under the hood, `syncToDataStore` uses Transform's listener API, which is basically the exact same thing as Transform, except it doesn't return anything, it's just used for observation and side effects. ```kotlin suspend fun StateStore.syncToDataStore(dataStore: DataStore) { listener { … } } ``` [24:33](https://www.youtube.com/watch?v=69QYEXymfVo&t=1473s) And then, within that listener, it reads every single key within the store, and it checks if one is a `PersistToDataStore` key. ```kotlin suspend fun StateStore.syncToDataStore(dataStore: DataStore) { listener { for (key in keys) { if (key is PersistToDataStore) { val value = getState(key).value LaunchedEffect(key) { // Restore saved value from dataStore on first access } LaunchedEffect(key, value) { // Flush updated value to dataStore on change } } } } } ``` [24:41](https://www.youtube.com/watch?v=69QYEXymfVo&t=1481s) When it is, it uses `LaunchedEffect` to sync those values back to `DataStore` whenever they change. [24:46](https://www.youtube.com/watch?v=69QYEXymfVo&t=1486s) And, the thing I want to point out here is that having a standard container like `StateStore` is what lets us provide these drop-in integrations. [24:51](https://www.youtube.com/watch?v=69QYEXymfVo&t=1491s) `DataStore` is just the first one we're looking at. ### Summary of Compose State categories [24:56](https://www.youtube.com/watch?v=69QYEXymfVo&t=1496s) So, stepping back to our state categories, we now have a pretty clear picture of how you can use Compose State across your entire application. [25:02](https://www.youtube.com/watch?v=69QYEXymfVo&t=1502s) From `mutableStateOf` for live state, to the SavedState APIs for screen-local presentation state, and `StateStore` for cross-component session state, and then lastly, we've got `collectAsState` for interop with Flows. [25:15](https://www.youtube.com/watch?v=69QYEXymfVo&t=1515s) But really, interop isn't limited to any single category, you can just use those helpers like `collectAsState` whenever you need to bridge observable types into Compose State. [25:24](https://www.youtube.com/watch?v=69QYEXymfVo&t=1524s) But for permanent state, one other thing we're looking at is ways to work with Compose or for `DataStore` and Room to directly expose Compose State, so you don't have to write your own like nicer syntax for doing that. [25:39](https://www.youtube.com/watch?v=69QYEXymfVo&t=1539s) And with that, I'm happy to announce that Transform, `StateStore`, and the `DataStore` integration are all available in alpha today, and that we've also published all the documentation on developer.android.com, and we've got an AppState Recipes repo on GitHub that shows examples of using both `StateStore` and Transform. ```toml [versions] appstate = "1.0.0-alpha01" ... [libraries] ... transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" } transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" } statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" } statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" } ``` [25:55](https://www.youtube.com/watch?v=69QYEXymfVo&t=1555s) And, yeah, please give them a try in your app. [25:58](https://www.youtube.com/watch?v=69QYEXymfVo&t=1558s) Give us feedback. [25:59](https://www.youtube.com/watch?v=69QYEXymfVo&t=1559s) Like, we're in alpha, so this is when we can really iterate and make changes that'll make sure that they work for your use cases.