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

Avoiding common coroutine mistakes in Compose (...

Avoiding common coroutine mistakes in Compose (KotlinConf '23)

Compose and coroutines work great together, but there are certain patterns you need to avoid when combining them. In this session, we’ll look at some common pitfalls, detail why and how they cause problems, and see what patterns or APIs should be used instead.

More info and resources: https://zsmb.co/appearances/kotlinconf-2023-day2/

Márton Braun

April 14, 2023
Tweet

More Decks by Márton Braun

Other Decks in Programming

Transcript

  1. Column { Row { Icon() Text() } Image() Row {

    Column { Text() Text() } } } Composition goo.gle/mad-skills-compose-phases
  2. Column { Row { Icon() Text() } Image() Row {

    Column { Text() Text() } } } Composition goo.gle/mad-skills-compose-phases
  3. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  4. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  5. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  6. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  7. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  8. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } } In the Composition Not in the Composition
  9. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { val coroutineScope = rememberCoroutineScope() Column { coroutineScope.launch { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } } In the Composition Not in the Composition
  10. @Composable fun HomeScreen( time: String, connected: Boolean, snackbarHostState: SnackbarHostState, )

    { Column { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } } @Composable fun HomeScreen( time: String, connected: Boolean, snackbarHostState: SnackbarHostState, ) { Column { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } } Creating coroutines in the composition val coroutineScope = rememberCoroutineScope() coroutineScope.launch {
  11. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { Column { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } } LaunchedEffect() {
  12. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { Column { LaunchedEffect() { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  13. Creating coroutines in the composition @Composable fun HomeScreen( time: String,

    connected: Boolean, snackbarHostState: SnackbarHostState, ) { Column { LaunchedEffect(connected, snackbarHostState) { val message = if (connected) "Connected and ready!" else "No connection :(" snackbarHostState.showSnackbar(message) } Text(text = "It's $time, a great time to order a pizza!") } }
  14. A positive example @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier =

    Modifier) { Box(modifier) { val listState = rememberLazyListState() LazyColumn(state = listState) { /* Show pizza items... */ } FloatingActionButton( onClick = { listState.animateScrollToItem(0) } ) { Icon(Icons.Filled.ArrowUpward) } } }
  15. A positive example @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier =

    Modifier) { Box(modifier) { val listState = rememberLazyListState() LazyColumn(state = listState) { /* Show pizza items... */ } FloatingActionButton( onClick = { listState.animateScrollToItem(0) } ) { Icon(Icons.Filled.ArrowUpward) } } }
  16. A positive example @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier =

    Modifier) { Box(modifier) { val listState = rememberLazyListState() LazyColumn(state = listState) { /* Show pizza items... */ } FloatingActionButton( onClick = { listState.animateScrollToItem(0) } ) { Icon(Icons.Filled.ArrowUpward) } } }
  17. A positive example @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier =

    Modifier) { Box(modifier) { val listState = rememberLazyListState() LazyColumn(state = listState) { /* Show pizza items... */ } FloatingActionButton( onClick = { listState.animateScrollToItem(0) } ) { Icon(Icons.Filled.ArrowUpward) } } }
  18. FloatingActionButton( onClick = { } ) { Icon(Icons.Filled.ArrowUpward) } }

    } listState.animateScrollToItem(0) A positive example @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier = Modifier) { Box(modifier) { val listState = rememberLazyListState() LazyColumn(state = listState) { /* Show pizza items... */ }
  19. A positive example FloatingActionButton( onClick = { } ) {

    Icon(Icons.Filled.ArrowUpward) } } } val scope = rememberCoroutineScope() listState.animateScrollToItem(0) LazyColumn(state = listState) { /* Show pizza items... */ } scope.launch { } @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier = Modifier) { Box(modifier) { val listState = rememberLazyListState()
  20. A positive example @Composable fun PizzaList(pizzas: List<Pizza>, modifier: Modifier =

    Modifier) { Box(modifier) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() LazyColumn(state = listState) { /* Show pizza items... */ } FloatingActionButton( onClick = { scope.launch { listState.animateScrollToItem(0) } } ) { Icon(Icons.Filled.ArrowUpward) } } }
  21. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { } 1 2 3 4 5 6 7 8 9 10 numbers
  22. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = numbers.map { it % 8 } } 1 2 3 4 5 6 7 8 9 10 numbers phaseFlow 1 2 3 4 5 6 7 0 1 2 map
  23. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = numbers.map { it % 8 } val phase by phaseFlow.collectAsState(initial = 0) }
  24. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = numbers.map { it % 8 } val phase: Int by phaseFlow.collectAsState(initial = 0) }
  25. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = numbers.map { it % 8 } val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) }
  26. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = numbers.map { it % 8 } val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) }
  27. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = numbers.map { it % 8 } val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) }
  28. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) } numbers.map { it % 8 }
  29. Creating Flows in the composition numbers.map { it % 8

    } @Composable fun RotatedPizzaSlice(phase: Int) @Composable fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) }
  30. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = remember(numbers) { } val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) } numbers.map { it % 8 }
  31. Creating Flows in the composition @Composable fun RotatedPizzaSlice(phase: Int) @Composable

    fun PizzaLoader(numbers: Flow<Int>) { val phaseFlow = remember(numbers) { } val phase by phaseFlow.collectAsState(initial = 0) RotatedPizzaSlice(phase) } numbers.map { it % 8 }
  32. Using StateFlow values class PizzaViewModel( repository: PizzaRepository ) : ViewModel()

    { val pizzas: StateFlow<List<Pizza>> = repository.getAllPizzas() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000L), initialValue = PizzaRepository.DEFAULT_PIZZAS, ) }
  33. Using StateFlow values class PizzaViewModel( repository: PizzaRepository ) : ViewModel()

    { val pizzas: StateFlow<List<Pizza>> = repository.getAllPizzas() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000L), initialValue = PizzaRepository.DEFAULT_PIZZAS, ) }
  34. Using StateFlow values @Composable fun PizzaList(pizzas: List<Pizza>) @Composable fun LivePizzaList(vm:

    PizzaViewModel) { val prices = vm.pizzas.value PizzaList(prices) }
  35. Using StateFlow values @Composable fun PizzaList(pizzas: List<Pizza>) @Composable fun LivePizzaList(vm:

    PizzaViewModel) { val prices = vm.pizzas.value PizzaList(prices) }
  36. Using StateFlow values @Composable fun PizzaList(pizzas: List<Pizza>) @Composable fun LivePizzaList(vm:

    PizzaViewModel) { val prices = vm.pizzas.value PizzaList(prices) }
  37. Using StateFlow values @Composable fun PizzaList(pizzas: List<Pizza>) @Composable fun LivePizzaList(vm:

    PizzaViewModel) { val prices by vm.pizzas.collectAsState() PizzaList(prices) }
  38. Using StateFlow values @Composable fun PizzaList(pizzas: List<Pizza>) @Composable fun LivePizzaList(vm:

    PizzaViewModel) { val prices by vm.pizzas.collectAsStateWithLifecycle() PizzaList(prices) } goo.gle/consume-flows-safely
  39. Summary • Watch out for what happens in the Composition

    • Use LaunchedEffect to call suspending functions • Collect StateFlows as State • Use collectAsStateWithLifecycle on Android
  40. Thank you, and don’t forget to vote! KotlinConf’23 Amsterdam Márton

    Braun @zsmb13 Developer Relations Engineer Google