Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Compose Without Flow - AndroidMakers 2026

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Compose Without Flow - AndroidMakers 2026

Avatar for remcomokveld

remcomokveld

April 10, 2026

More Decks by remcomokveld

Other Decks in Technology

Transcript

  1. DNA.inc Android Makers Paris 2026 Compose 
 Without Flow Unidirectional

    State with Snapshots and Courtines Remco Mokveld Staff Engineer - Android
  2. DNA.inc Compose Without Flow Unidirectional Data Flow 0. Current app

    data New app data with the bookmarked article UI state 1. Current UI state 6. New UI state The data layer persists the data change 
 and updates the application data Data layer View model UI Layer UI elements The ViewModel notifies the state change to the data layer User bookmarks article (UI event)
  3. DNA.inc Two Observability Paradigms One-shot data source with State Flow

    1 class NewsViewModel(...) : ViewModel() { 2 private val _state = MutableStateFlow<List<Article>?>(null) 3 val state = _state.asStateFlow() 4 5 fun load() { 6 viewModelScope.launch { 7 _state.value = repository.getLatestArticles() 8 } 9 } 10 }
  4. DNA.inc Two Observability Paradigms One-shot data source with State Flow

    1 class NewsViewModel(...) : ViewModel() { 2 private val _state = MutableStateFlow<List<Article>?>(null) 3 val state = _state.asStateFlow() 4 5 fun load() { 6 viewModelScope.launch { 7 _state.value = repository.getLatestArticles() 8 } 9 } 10 }
  5. DNA.inc Two Observability Paradigms One-shot data source with State Flow

    1 class NewsViewModel(...) : ViewModel() { 2 private val _state = MutableStateFlow<List<Article>?>(null) 3 val state = _state.asStateFlow() 4 5 fun load() { 6 viewModelScope.launch { 7 _state.value = repository.getLatestArticles() 8 } 9 } 10 }
  6. DNA.inc Two Observability Paradigms One-shot data source with State Flow

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 val articles by viewModel.state.collectAsStateWithLifecycle() 5 LazyColumn { 6 items(articles) { article -> ArticleListItem(article) } 7 } 8 }
  7. DNA.inc Two Observability Paradigms One-shot data source with State Flow

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 val articles by viewModel.state.collectAsStateWithLifecycle() 5 LazyColumn { 6 items(articles) { article -> ArticleListItem(article) } 7 } 8 }
  8. DNA.inc Two Observability Paradigms One-shot data source with State Flow

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 val articles by viewModel.state.collectAsStateWithLifecycle() 5 LazyColumn { 6 items(articles) { article -> ArticleListItem(article) } 7 } 8 }
  9. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 class NewsViewModel(...): ViewModel() { 2 var state by mutableStateOf<List<Article>?>(null) 3 private set 4 5 fun load() { 6 viewModelScope.launch { 6 val articles = repository.getLatestArticles() 7 withMutableSnapshot { state = articles } 8 } 9 } 10 }
  10. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 class NewsViewModel(...): ViewModel() { 2 var state by mutableStateOf<List<Article>?>(null) 3 private set 4 5 fun load() { 6 viewModelScope.launch { 7 withMutableSnapshot { 8 state = repository.getLatestArticles() 9 } 10 } 11 } 12 }
  11. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 class NewsViewModel(...): ViewModel() { 2 var state by mutableStateOf<List<Article>?>(null) 3 private set 4 5 fun load() { 6 viewModelScope.launch { 7 withMutableSnapshot { 8 state = repository.getLatestArticles() 9 } 10 } 11 } 12 }
  12. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 LazyColumn { 5 items(viewModel.state.orEmpty()) { article -> ..} 6 } 7 }
  13. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 LazyColumn { 5 items(viewModel.state.orEmpty()) { article -> ..} 6 } 7 }
  14. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 LazyColumn { 5 items(viewModel.state.orEmpty()) { article -> ..} 6 } 7 }
  15. DNA.inc Two Observability Paradigms One-shot data source with Compose State

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 LaunchedEffect(viewModel) { viewModel.load() } 4 LazyColumn { 5 items(viewModel.state.orEmpty()) { article -> ..} 6 } 7 }
  16. DNA.inc Two Observability Paradigms Streaming data source with State Flow

    1 class NewsViewModel(...): ViewModel() { 2 val state = repository.getLatestArticles() 3 .stateIn(viewModelScope, WhileSubscribed(5.seconds), null) 4 }
  17. DNA.inc Two Observability Paradigms Streaming data source with State Flow

    1 class NewsViewModel(...): ViewModel() { 2 val state = repository.getLatestArticles() 3 .stateIn(viewModelScope, WhileSubscribed(5.seconds), null) 4 }
  18. DNA.inc Two Observability Paradigms Streaming data source with State Flow

    1 class NewsViewModel(...): ViewModel() { 2 val state = repository.getLatestArticles() 3 .stateIn(viewModelScope, WhileSubscribed(5.seconds), null) 4 }
  19. DNA.inc Two Observability Paradigms Streaming data source with State Flow

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 val articles by viewModel.state.collectAsStateWithLifecycle() 4 LazyColumn { 5 items(articles.orEmpty()) { article -> } 6 } 7 }
  20. DNA.inc Two Observability Paradigms Streaming data source with State Flow

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 3 val articles by viewModel.state.collectAsStateWithLifecycle() 4 LazyColumn { 5 items(articles.orEmpty()) { article -> } 6 } 7 }
  21. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> ... } 8 .stateIn(...)
  22. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> ... } 8 .stateIn(...)
  23. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> ... } 8 .stateIn(...)
  24. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> ... } 8 .stateIn(...)
  25. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> ... } 8 .stateIn(...)
  26. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> ... } 8 .stateIn(...)
  27. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 @Composable 2 fun NewsList(viewModel: NewsViewModel) { 4 val state by viewModel.state.collectAsState() 5 LazyColumn { 6 items(state.articles) { article -> ArticleListItem(article) } 7 } 8 }
  28. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task = tasksRepository.getTaskStream(taskId) 3 4 val uiState: StateFlow<TaskDetailUiState> = combine( 5 snapshotFlow { _isTaskDeleted }, 6 _task 7 ) { isTaskDeleted, task -> TaskDetailUiState(task, isTaskDeleted) } 8 .stateIn(...)
  29. DNA.inc Two Observability Paradigms Streaming data source with Compose State

    1 private var _isTaskDeleted by mutableStateOf(false) 2 private val _task get() = tasksRepository.getTask(taskId) 3 4 val uiState: TaskDetailUiState get() = TaskDetailUiState(_task, _isTaskDeleted)
  30. DNA.inc Flow’s Responsibilities Flow models a stream 
 of values

    over time Compose State models 
 a current value
  31. DNA.inc Flow’s Responsibilities Flow vs Compose State Flow State +

    Execution Collection drives work Compose State State only No execution model
  32. DNA.inc Flow’s Responsibilities Who drives the work? Flow Start when

    collected Stop when cancelled Compose State Read current value Re-read when invalidated
  33. DNA.inc Compose State Propagation How does compose propagate change? No

    streams No collection No operators Compose uses the snapshot system
  34. DNA.inc Compose State Propagation The Snapshot System 
 (high-level) Foundation

    of Compose’s state model State is versioned Reads happen within a snapshot Writes are isolated Changes are applied atomically Observers react after apply
  35. DNA.inc Compose State Propagation What is a Snapshot? A consistent

    view of state at a point in time Reads always return values from the same version Later changes are not visible
  36. DNA.inc Compose State Propagation What is a Snapshot? 1 var

    state by mutableStateOf(1) 2 3 val snapshot = Snapshot.takeSnapshot() 4 5 state = 2 6 7 snapshot.enter { 8 println(state) 9 }
  37. DNA.inc Compose State Propagation What is a Snapshot? 1 var

    state by mutableStateOf(1) 2 3 val snapshot = Snapshot.takeSnapshot() 4 5 state = 2 6 7 snapshot.enter { 8 println(state) 9 }
  38. DNA.inc Compose State Propagation What is a Snapshot? 1 var

    state by mutableStateOf(1) 2 3 val snapshot = Snapshot.takeSnapshot() 4 5 state = 2 6 7 snapshot.enter { 8 println(state) 9 }
  39. DNA.inc Compose State Propagation What is a Snapshot? 1 var

    state by mutableStateOf(1) 2 3 val snapshot = Snapshot.takeSnapshot() 4 5 state = 2 6 7 snapshot.enter { 8 println(state) 9 }
  40. DNA.inc Compose State Propagation What is a Snapshot? 1 var

    state by mutableStateOf(1) 2 3 val snapshot = Snapshot.takeSnapshot() 4 5 state = 2 6 7 snapshot.enter { 8 println(state) 9 }
  41. DNA.inc Compose State Propagation What is a Snapshot? 1 var

    state by mutableStateOf(1) 2 3 val snapshot = Snapshot.takeSnapshot() 4 5 state = 2 6 7 snapshot.enter { 8 println(state) // still 1 9 }
  42. DNA.inc Compose State Propagation Do writes change global state? 1

    val snapshot = Snapshot.takeMutableSnapshot() 2 3 snapshot.enter { 4 state = 1 5 } 6 7 println(state) // still old value
  43. DNA.inc Compose State Propagation Do writes change global state? 1

    val snapshot = Snapshot.takeMutableSnapshot() 2 3 snapshot.enter { 4 state = 1 5 } 6 7 println(state) // still old value
  44. DNA.inc Compose State Propagation Do writes change global state? 1

    val snapshot = Snapshot.takeMutableSnapshot() 2 3 snapshot.enter { 4 state = 1 5 } 6 7 println(state) // still old value
  45. DNA.inc Compose State Propagation When does state change? 1 val

    snapshot = Snapshot.takeMutableSnapshot() 2 3 snapshot.enter { 4 state = 1 5 } 6 7 snapshot.apply() 8 9 println(state) // now updated
  46. DNA.inc Compose State Propagation Entering a snapshot makes it the

    current snapshot val snapshot = Snapshot.takeSnapshot() check(snapshot != Snapshot.current) snapshot.enter { check(snapshot == Snapshot,current) }
  47. DNA.inc Compose State Propagation Snapshot tracks state reads/writes 1 class

    MutableStateImpl<T> { 2 override val value: T 3 get() { 4 Snapshot.current.onStateRead(this) 5 .. 6 } 3 set(value: T) { 4 Snapshot.current.onStateWritten(this) 5 .. 6 } 7 }
  48. DNA.inc Compose State Propagation Snapshot tracks state reads/writes 1 class

    MutableStateImpl<T> { 2 override val value: T 3 get() { 4 Snapshot.current.onStateRead(this) 5 .. 6 } 3 set(value: T) { 4 Snapshot.current.onStateWritten(this) 5 .. 6 } 7 }
  49. DNA.inc Compose State Propagation Apply notifies observers 1 Snapshot.registerApplyObserver {

    changedStates, _ -> 2 changedStates.forEach { println("Changed: $it") } 3 } 4 5 Snapshot.withMutableSnapshot { 6 state = 4 7 }
  50. DNA.inc Compose State Propagation Apply notifies observers 1 Snapshot.registerApplyObserver {

    changedStates, _ -> 2 changedStates.forEach { println("Changed: $it") } 3 } 4 5 Snapshot.withMutableSnapshot { 6 state = 4 7 }
  51. DNA.inc Compose State Propagation Apply notifies observers 1 Snapshot.registerApplyObserver {

    changedStates, _ -> 2 changedStates.forEach { println("Changed: $it") } 3 } 4 5 Snapshot.withMutableSnapshot { 6 state = 4 7 }
  52. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  53. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  54. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  55. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  56. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  57. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  58. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  59. DNA.inc Compose State Propagation So how does Compose know what

    changed? State reads Composable reads State Snapshot tracks read states State writes State 
 Changes Write 
 Tracked Changed 
 states reported apply() Compose 
 recomposes
  60. DNA.inc Compose State Propagation What this means Reads are tracked

    automatically Writes are tracked automatically Dependencies are implicit
  61. DNA.inc Compose State Propagation No streams needed Side-effect free state

    production pipeline Mapping can be cached with derivedStateOf Backpressure is no longer a problem
  62. DNA.inc Compose State Propagation What does this enable? State can

    live anywhere Reads are tracked automatically Updates propagate without streams
  63. DNA.inc Compose State Propagation What does this enable? State can

    live anywhere Reads are tracked automatically Updates propagate without streams What if our repositories just owned state?
  64. DNA.inc Repositories Owning State Repository owns State 1 @Stable 2

    class ArticlesRepository( 3 private val dataSource: ArticlesDataSource 4 ) { 5 var articles: List<Article>? by mutableStateOf(null) 6 private set 7 8 suspend fun load() { 9 val result = dataSource.getLatestArticles() 10 Snapshot.withMutableSnapshot { articles = result } 11 } 12 }
  65. DNA.inc Repositories Owning State Repository owns State 1 @Stable 2

    class ArticlesRepository( 3 private val dataSource: ArticlesDataSource 4 ) { 5 var articles: List<Article>? by mutableStateOf(null) 6 private set 7 8 suspend fun load() { 9 val result = dataSource.getLatestArticles() 10 Snapshot.withMutableSnapshot { articles = result } 11 } 12 }
  66. DNA.inc Repositories Owning State Repository owns State 1 @Stable 2

    class ArticlesRepository( 3 private val dataSource: ArticlesDataSource 4 ) { 5 var articles: List<Article>? by mutableStateOf(null) 6 private set 7 8 suspend fun load() { 9 val result = dataSource.getLatestArticles() 10 Snapshot.withMutableSnapshot { articles = result } 11 } 12 }
  67. DNA.inc Repositories Owning State Repository owns State 1 @Stable 2

    class UserRepository { 3 var favorites = mutableStateMapOf<String, Boolean>() 4 5 fun setFavorite(id: String, favorite: Boolean) { 6 Snapshot.withMutableSnapshot { 7 favorites[id] = favorite 8 } 9 } 10 }
  68. DNA.inc Repositories Owning State NewsScreen with local filter 1 @Composable

    2 fun NewsScreen(viewModel: NewsViewModel) { 3 LazyColumn { 4 item { 5 TextField(viewModel.searchQuery, { viewModel.searchQuery = it }) 6 } 7 items(viewModel.articles) { article -> ArticleListItem(article) } 8 } 9 }
  69. DNA.inc Repositories Owning State NewsScreen with local filter 1 @Composable

    2 fun NewsScreen(viewModel: NewsViewModel) { 3 LazyColumn { 4 item { 5 TextField(viewModel.searchQuery, { viewModel.searchQuery = it }) 6 } 7 items(viewModel.articles) { article -> ArticleListItem(article) } 8 } 9 }
  70. DNA.inc Repositories Owning State ViewModel maps data to UI state

    1 @Stable 2 class NewsViewModel( 3 private val articleRepository: ArticleRepository, 4 private val userRepository: UserRepository 5 ) { 6 var searchQuery by mutableStateOf("") 7 8 val articles: List<ArticleUiState> 9 get() = articleRepository.articles 10 .filter { it.title.contains(searchQuery) } 11 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 12 }
  71. DNA.inc Repositories Owning State ViewModel maps data to UI state

    1 @Stable 2 class NewsViewModel( 3 private val articleRepository: ArticleRepository, 4 private val userRepository: UserRepository 5 ) { 6 var searchQuery by mutableStateOf("") 7 8 val articles: List<ArticleUiState> 9 get() = articleRepository.articles 10 .filter { it.title.contains(searchQuery) } 11 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 12 }
  72. DNA.inc Repositories Owning State ViewModel maps data to UI state

    1 @Stable 2 class NewsViewModel( 3 private val articleRepository: ArticleRepository, 4 private val userRepository: UserRepository 5 ) { 6 var searchQuery by mutableStateOf("") 7 8 val articles: List<ArticleUiState> 9 get() = articleRepository.articles 10 .filter { it.title.contains(searchQuery) } 11 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 12 }
  73. DNA.inc Repositories Owning State ViewModel maps data to UI state

    1 @Stable 2 class NewsViewModel( 3 private val articleRepository: ArticleRepository, 4 private val userRepository: UserRepository 5 ) { 6 var searchQuery by mutableStateOf("") 7 8 val articles: List<ArticleUiState> 9 get() = articleRepository.articles 10 .filter { it.title.contains(searchQuery) } 11 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 12 }
  74. DNA.inc Repositories Owning State ViewModel maps data to UI state

    1 @Stable 2 class NewsViewModel( 3 private val articleRepository: ArticleRepository, 4 private val userRepository: UserRepository 5 ) { 6 var searchQuery by mutableStateOf("") 7 8 val articles: List<ArticleUiState> 9 get() = articleRepository.articles 10 .filter { it.title.contains(searchQuery) } 11 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 12 }
  75. DNA.inc Repositories Owning State Combining multiple sources 1 @Composable 2

    fun NewsScreen(viewModel: NewsViewModel) { 3 LazyColumn { 4 item { 5 TextField(viewModel.searchQuery, { viewModel.searchQuery = it }) 6 } 7 items(viewModel.articles) { article -> ArticleListItem(article) } 8 } 9 }
  76. DNA.inc Repositories Owning State Combining multiple sources 1 @Composable 2

    fun NewsScreen(viewModel: NewsViewModel) { 3 LazyColumn { 4 item { 5 TextField(viewModel.searchQuery, { viewModel.searchQuery = it }) 6 } 7 items(viewModel.articles) { article -> ArticleListItem(article) } 8 } 9 }
  77. DNA.inc Repositories Owning State Flow vs this approach articlesFlow favoritesFlow

    searchQueryFlow combine + map articles favorites searchQuery normal Kotlin logic Same inputs. Same output. Different mechanism. stateIn visibleArticles visibleArticles
  78. DNA.inc Repositories Owning State Reactive without streams Multiple changing inputs

    Automatic dependency tracking Re-evaluation on change
  79. DNA.inc Flow’s Responsibilities Removing Flow splits responsibilities propagate state changes

    → Snapshot System lifecycle-aware activation → Remembers/Retained StateOwners
  80. DNA.inc State Activation So How Do We Handle Activation? Work

    must be started explicitly Work must be stopped explicitly
  81. DNA.inc State Activation RememberObserver 1 class MyStateOwner { 2 fun

    activate() = Unit 3 fun deactivate() = Unit 4 } 5 6 @Composable 7 fun rememberMyStateOwner(): MyStateOwner = 8 remember { 9 object : RememberObserver { 10 val stateOwner = MyStateOwner() 11 override fun onRemembered() = stateProducer.activate()
 12 override fun onForgotten() = stateProducer.deactivate()
 13 }
 14 }.stateOwner
  82. DNA.inc State Activation RememberObserver 1 class MyStateOwner { 2 fun

    activate() = Unit 3 fun deactivate() = Unit 4 } 5 6 @Composable 7 fun rememberMyStateOwner(): MyStateOwner = 8 remember { 9 object : RememberObserver { 10 val stateOwner = MyStateOwner() 11 override fun onRemembered() = stateProducer.activate()
 12 override fun onForgotten() = stateProducer.deactivate()
 13 }
 14 }.stateOwner
  83. DNA.inc State Activation RememberObserver 1 class MyStateOwner { 2 fun

    activate() = Unit 3 fun deactivate() = Unit 4 } 5 6 @Composable 7 fun rememberMyStateOwner(): MyStateOwner = 8 remember { 9 object : RememberObserver { 10 val stateOwner = MyStateOwner() 11 override fun onRemembered() = stateProducer.activate()
 12 override fun onForgotten() = stateProducer.deactivate()
 13 }
 14 }.stateOwner
  84. DNA.inc State Activation RememberObserver 1 class MyStateOwner { 2 fun

    activate() = Unit 3 fun deactivate() = Unit 4 } 5 6 @Composable 7 fun rememberMyStateOwner(): MyStateOwner = 8 remember { 9 object : RememberObserver { 10 val stateOwner = MyStateOwner() 11 override fun onRemembered() = stateProducer.activate()
 12 override fun onForgotten() = stateProducer.deactivate()
 13 }
 14 }.stateOwner
  85. DNA.inc State Activation RememberObserver 1 class MyStateOwner { 2 fun

    activate() = Unit 3 fun deactivate() = Unit 4 } 5 6 @Composable 7 fun rememberMyStateOwner(): MyStateOwner = 8 remember { 9 object : RememberObserver { 10 val stateOwner = MyStateOwner() 11 override fun onRemembered() = stateProducer.activate()
 12 override fun onForgotten() = stateProducer.deactivate()
 13 }
 14 }.stateOwner
  86. DNA.inc State Activation RememberObserver 1 class MyStateOwner { 2 fun

    activate() = Unit 3 fun deactivate() = Unit 4 } 5 6 @Composable 7 fun rememberMyStateOwner(): MyStateOwner = 8 remember { 9 object : RememberObserver { 10 val stateOwner = MyStateOwner() 11 override fun onRemembered() = stateProducer.activate()
 12 override fun onForgotten() = stateProducer.deactivate()
 13 }
 14 }.stateOwner
  87. DNA.inc State Activation Lifecycle-aware activation 1 @Composable 2 fun NewsScreen()

    { 3 val repository = retainStateProducerWithLifecycle { 4 NewsRepository(dataSource) 5 } 6 7 val articles = repository.articles 8 9 LazyColumn { 10 items(articles) { article -> 11 ArticleListItem(article) 12 } 13 } 14 }
  88. DNA.inc State Activation Explicit activation 1 override suspend fun activate()

    { 2 while (currentCoroutineContext().isActive) { 3 val result = dataSource.getLatestArticles() 4 5 Snapshot.withMutableSnapshot { 6 articles = result 7 } 8 9 delay(1.minutes) 10 } 11 }
  89. DNA.inc Compose Without Flow Why? One observability framework instead of

    two Simplified state modelling Side-effect free state State is, or is not
  90. DNA.inc Why? Simpler Modelling Flow Repository → Flow<List<Article>> ViewModel →

    StateFlow UI → collectAsState() Derived values → Flow operators Compose State Repository → List<Article> ViewModel → List<ArticleUiState> UI → Reads State Derived Values → Normal Kotlin
  91. DNA.inc Why? Granularity of Updates Flow Emits one big UI

    state Change = new object Recomposition at emission boundary Compose State Tracks individual reads Change = specific field Recomposition at read site
  92. DNA.inc Why? Granularity of updates 1 class NewsViewModel( 2 private

    val articleRepository: ArticleRepository, 3 private val userRepository: UserRepository 4 ) { 5 var searchQuery by mutableStateOf("") 6 7 val articles: List<ArticleUiState> 8 get() = articleRepository.articles 9 .filter { it.title.contains(searchQuery) } 10 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 11 }
  93. DNA.inc Why? Granularity of updates 1 class NewsViewModel( 2 private

    val articleRepository: ArticleRepository, 3 private val userRepository: UserRepository 4 ) { 5 var searchQuery by mutableStateOf("") 6 7 val articles: List<ArticleUiState> 8 get() = articleRepository.articles 9 .filter { it.title.contains(searchQuery) } 10 .map { ArticleUiState(it, userRepository.isFavorite(it)) } 11 }
  94. DNA.inc Why? Granularity of updates 1 class NewsViewModel( 2 private

    val articleRepository: ArticleRepository, 3 private val userRepository: UserRepository 4 ) { 5 var searchQuery by mutableStateOf("") 6 7 val articles: List<ArticleUiState> 8 get() = articleRepository.articles 9 .filter { it.title.contains(searchQuery) } 10 .map { ArticleUiState(it, userRepository::isFavorite) } 11 }
  95. DNA.inc Why? Granularity of updates 1 class NewsViewModel( 2 private

    val articleRepository: ArticleRepository, 3 private val userRepository: UserRepository 4 ) { 5 var searchQuery by mutableStateOf("") 6 7 val articles: List<ArticleUiState> 8 get() = articleRepository.articles 9 .filter { it.title.contains(searchQuery) } 10 .map { ArticleUiState(it, userRepository::isFavorite) } 11 }
  96. DNA.inc State Activation State is or is not Combine with

    4 flows as input will never emit if one of them hangs Map functions in flow can suspend
  97. DNA.inc State Activation Why not? State reads are hard to

    validate at compile time Compose State belongs to the UI layer
  98. DNA.inc Compose Without Flow Thanks! Let’s talk about Compose Without

    Flow [email protected] Bluesky @rmokveld.bsky.social X @remmies Linkedin linkedin.com/remco-mokveld