Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Avoiding common coroutine mistakes in Compose

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Lint is pretty good.

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Composable functions can... ● Be called very frequently

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Creating coroutines in the composition

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Creating coroutines in composition

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

A positive example

Slide 27

Slide 27 text

A positive example

Slide 28

Slide 28 text

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) } } }

Slide 29

Slide 29 text

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) } } }

Slide 30

Slide 30 text

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) } } }

Slide 31

Slide 31 text

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) } } }

Slide 32

Slide 32 text

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... */ }

Slide 33

Slide 33 text

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()

Slide 34

Slide 34 text

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) } } }

Slide 35

Slide 35 text

Creating Flows in the composition

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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) }

Slide 41

Slide 41 text

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) }

Slide 42

Slide 42 text

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) }

Slide 43

Slide 43 text

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) }

Slide 44

Slide 44 text

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) }

Slide 45

Slide 45 text

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 }

Slide 46

Slide 46 text

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) }

Slide 47

Slide 47 text

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 }

Slide 48

Slide 48 text

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 }

Slide 49

Slide 49 text

Using StateFlow values

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Using StateFlow values

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Using StateFlow values

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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