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

Avoiding common coroutine mistakes in Compose (KotlinConf '23)

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/

Marton Braun

April 14, 2023
Tweet

More Decks by Marton Braun

Other Decks in Programming

Transcript

  1. Márton Braun @zsmb13
    Developer Relations Engineer
    Google
    Avoiding common
    coroutine mistakes
    in Compose
    KotlinConf’23
    Amsterdam

    View Slide

  2. Avoiding common
    coroutine mistakes
    in Compose

    View Slide

  3. Avoiding common
    coroutine mistakes
    in Compose
    Data layer
    UI layer
    UI elements
    ViewModel

    View Slide

  4. Avoiding common
    coroutine mistakes
    in Compose
    UI layer
    ViewModel
    Data layer
    UI elements

    View Slide

  5. Lint is pretty good.

    View Slide

  6. Data UI
    Composition Layout Drawing
    Compose phases
    goo.gle/mad-skills-compose-phases

    View Slide

  7. Data UI
    Composition Layout Drawing
    Compose phases
    goo.gle/mad-skills-compose-phases

    View Slide

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

    View Slide

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

    View Slide

  10. Composable functions can...
    ● Be called very frequently

    View Slide

  11. Composable functions can...
    ● Be called very frequently
    ● Execute in any order

    View Slide

  12. Composable functions can...
    ● Be called very frequently
    ● Execute in any order
    ● Run in parallel*

    View Slide

  13. Creating coroutines in the composition

    View Slide

  14. 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!")
    }
    }

    View Slide

  15. 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!")
    }
    }

    View Slide

  16. 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!")
    }
    }

    View Slide

  17. 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!")
    }
    }

    View Slide

  18. Creating coroutines in composition

    View Slide

  19. 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!")
    }
    }

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. @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 {

    View Slide

  23. 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() {

    View Slide

  24. 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!")
    }
    }

    View Slide

  25. 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!")
    }
    }

    View Slide

  26. A positive example

    View Slide

  27. A positive example

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. 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, modifier: Modifier = Modifier) {
    Box(modifier) {
    val listState = rememberLazyListState()

    View Slide

  34. A positive example
    @Composable
    fun PizzaList(pizzas: List, 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)
    }
    }
    }

    View Slide

  35. Creating Flows in the composition

    View Slide

  36. Creating Flows in the composition
    @Composable
    fun RotatedPizzaSlice(phase: Int)
    0 3
    2
    1
    4 7
    6
    5

    View Slide

  37. Creating Flows in the composition
    @Composable
    fun RotatedPizzaSlice(phase: Int)
    @Composable
    fun PizzaLoader(numbers: Flow) {
    }

    View Slide

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

    View Slide

  39. Creating Flows in the composition
    @Composable
    fun RotatedPizzaSlice(phase: Int)
    @Composable
    fun PizzaLoader(numbers: Flow) {
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  49. Using StateFlow values

    View Slide

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

    View Slide

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

    View Slide

  52. Using StateFlow values
    @Composable
    fun PizzaList(pizzas: List)

    View Slide

  53. Using StateFlow values
    @Composable
    fun PizzaList(pizzas: List)
    @Composable
    fun LivePizzaList(vm: PizzaViewModel) {
    }

    View Slide

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

    View Slide

  55. Using StateFlow values

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  59. Using StateFlow values
    @Composable
    fun PizzaList(pizzas: List)
    @Composable
    fun LivePizzaList(vm: PizzaViewModel) {
    val prices by vm.pizzas.collectAsStateWithLifecycle()
    PizzaList(prices)
    }
    goo.gle/consume-flows-safely

    View Slide

  60. Using StateFlow values

    View Slide

  61. Summary
    ● Watch out for what happens in the Composition
    ● Use LaunchedEffect to call suspending functions
    ● Collect StateFlows as State
    ● Use collectAsStateWithLifecycle on Android

    View Slide

  62. Resources
    ● goo.gle/mad-skills-compose-phases
    ● goo.gle/compose-side-effects
    ● goo.gle/consume-flows-safely

    View Slide

  63. Thank you, and
    don’t forget
    to vote!
    KotlinConf’23
    Amsterdam
    Márton Braun @zsmb13
    Developer Relations Engineer
    Google

    View Slide