Slide 1

Slide 1 text

Mastering Recompositions in Compose

Slide 2

Slide 2 text

Mastering Recompositions in Compose 1. ­ How Compose works

Slide 3

Slide 3 text

Mastering Recompositions in Compose 1. ­ How Compose works 2. ­ Recomposition

Slide 4

Slide 4 text

Mastering Recompositions in Compose 1. ­ How Compose works 2. ­ Recomposition 3. ­ Debugging

Slide 5

Slide 5 text

Mastering Recompositions in Compose 1. ­ How Compose works 2. ­ Recomposition 3. ­ Debugging 4. ­ Problems and solutions

Slide 6

Slide 6 text

1. How Compose works

Slide 7

Slide 7 text

UiState( user = "user", pass = "pass", buttonEnabled = true ) user pass CLICK HERE

Slide 8

Slide 8 text

buttonEnabled = false UiState( 1 user = "user", 2 pass = "pass", 3 4 ) 5 user pass CAN'T CLICK

Slide 9

Slide 9 text

The biggest challenge Updating the UI without redrawing the whole screen

Slide 10

Slide 10 text

How Jetpack Compose works

Slide 11

Slide 11 text

How Jetpack Compose works ­ Specific implementation of interface declarative paradigm

Slide 12

Slide 12 text

How Jetpack Compose works ­ Specific implementation of interface declarative paradigm ­ Widgets in screen are functions

Slide 13

Slide 13 text

How Jetpack Compose works ­ Specific implementation of interface declarative paradigm ­ Widgets in screen are functions ­ Widgets are stateless

Slide 14

Slide 14 text

How Jetpack Compose works ­ Specific implementation of interface declarative paradigm ­ Widgets in screen are functions ­ Widgets are stateless ­ State provided by function arguments

Slide 15

Slide 15 text

@Composable fun MoviesList( val title: String, val movies: List ) { // Implementation } When state changes, UI is redrawn Otherwise, redrawing should be skipped

Slide 16

Slide 16 text

Three phases Data Composition Layout Drawing UI

Slide 17

Slide 17 text

Composition Taken from Android Developers blog

Slide 18

Slide 18 text

Layout Taken from Android Developers blog

Slide 19

Slide 19 text

Drawing Taken from Android Developers blog

Slide 20

Slide 20 text

2. Recomposition

Slide 21

Slide 21 text

Composable Lifecycle

Slide 22

Slide 22 text

When is the state changed?

Slide 23

Slide 23 text

When is the state changed? ­ It's hard to reason about it

Slide 24

Slide 24 text

When is the state changed? ­ It's hard to reason about it ­ Algorithm is confusing

Slide 25

Slide 25 text

When is the state changed? ­ It's hard to reason about it ­ Algorithm is confusing ­ It sometimes go against logic

Slide 26

Slide 26 text

When is the state changed? ­ It's hard to reason about it ­ Algorithm is confusing ­ It sometimes go against logic What does it mean that state is changed?

Slide 27

Slide 27 text

Stability If State is stable -> Compose can skip recompositions

Slide 28

Slide 28 text

When is a state stable?

Slide 29

Slide 29 text

When is a state stable? ­ All primitive types: Boolean, Int, Long, Float, Char...

Slide 30

Slide 30 text

When is a state stable? ­ All primitive types: Boolean, Int, Long, Float, Char... ­ Strings

Slide 31

Slide 31 text

When is a state stable? ­ All primitive types: Boolean, Int, Long, Float, Char... ­ Strings ­ All functional types (lambdas)

Slide 32

Slide 32 text

When is a state stable? ­ All primitive types: Boolean, Int, Long, Float, Char... ­ Strings ­ All functional types (lambdas) ­ Immutable classes using previous types

Slide 33

Slide 33 text

A stable class data class Movie( val id: Int, val title: String, val description: String )

Slide 34

Slide 34 text

When is a state unstable? Almost any other case:

Slide 35

Slide 35 text

When is a state unstable? Almost any other case: ­ Collections are not stable

Slide 36

Slide 36 text

When is a state unstable? Almost any other case: ­ Collections are not stable ­ Mutable classes

Slide 37

Slide 37 text

When is a state unstable? Almost any other case: ­ Collections are not stable ­ Mutable classes ­ All classes from modules where Compose compiler is not run

Slide 38

Slide 38 text

An unstable class data class Movie( val id: Int, val title: String, var description: String )

Slide 39

Slide 39 text

An unstable class var description: String data class Movie( 1 val id: Int, 2 val title: String, 3 4 ) 5

Slide 40

Slide 40 text

An unstable Composable @Composable fun MoviesList( val title: String, val movies: List ) { // Implementation }

Slide 41

Slide 41 text

An unstable Composable val movies: List @Composable 1 fun MoviesList( 2 val title: String, 3 4 ) { 5 // Implementation 6 } 7

Slide 42

Slide 42 text

3. Debugging

Slide 43

Slide 43 text

Find the problem Layout Inspector

Slide 44

Slide 44 text

Find the problem Layout Inspector

Slide 45

Slide 45 text

Find the reason Stability reports Generates reports in build time Classes Composables

Slide 46

Slide 46 text

Stability reports - Configuration kotlinOptions { val composeReportsDir = "compose_reports" freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.layout.buildDirectory.get().dir(composeReportsDir).asFile.absolutePath, "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.layout.buildDirectory.get().dir(composeReportsDir).asFile.absolutePath, ) }

Slide 47

Slide 47 text

Classes report Stable class stable class Movie { stable val id: Int stable val title: String stable val overview: String stable val releaseDate: String stable val poster: String stable val backdrop: String? stable val originalTitle: String stable val originalLanguage: String stable val popularity: Double stable val voteAverage: Double stable val isFavorite: Boolean = Stable }

Slide 48

Slide 48 text

Classes report Unstable class unstable class UiState { stable val loading: Boolean unstable val movies: List = Unstable }

Slide 49

Slide 49 text

Composables report Skippable composable restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MovieDetail( stable movie: Movie stable modifier: Modifier? = @static Companion )

Slide 50

Slide 50 text

Composables report Unskippable composable restartable scheme("[androidx.compose.ui.UiComposable]") fun ErrorText( unstable error: Throwable stable modifier: Modifier )

Slide 51

Slide 51 text

4. Problems and Solutions

Slide 52

Slide 52 text

4. Problems and Solutions 1. Unstable class 2. External unstable class 3. Lambdas 4. LazyList 5. Columns 6. State continuously changing 7. Modifiers

Slide 53

Slide 53 text

1. Unstable class data class Contact(var name: String)

Slide 54

Slide 54 text

1. Unstable class @Stable 1 data class Contact(var name: String) 2

Slide 55

Slide 55 text

2. External unstable class Collections, Other modules, 3rd party libraries Option 1: Stable wrapper @Stable class ListWrapper { val movies: List }

Slide 56

Slide 56 text

2. External unstable class Collections, Other modules, 3rd party libraries Option 2 (recommended): Stability config file // stability-config.txt kotlin.collections.* kotlin.Throwable

Slide 57

Slide 57 text

kotlinOptions { freeCompilerArgs += listOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + "$rootDir/stability-config.txt" ) }

Slide 58

Slide 58 text

3. Lambdas Lambdas are stable But they're not memoized (remembered) by default Only if scope is stable

Slide 59

Slide 59 text

3. Lambdas @Composable fun MoviesScreen() { val viewModel = viewModel { MoviesViewModel() } val state by viewModel.state.collectAsState() MoviesListWithButton( movies = state.movies, onButtonClick = { viewModel.onAddMovie() }, onMovieClick = { viewModel.onMovieClick() }, ) }

Slide 60

Slide 60 text

3. Lambdas val viewModel = viewModel { MoviesViewModel() } onButtonClick = { viewModel.onAddMovie() }, onMovieClick = { viewModel.onMovieClick() }, @Composable 1 fun MoviesScreen() { 2 3 val state by viewModel.state.collectAsState() 4 5 MoviesListWithButton( 6 movies = state.movies, 7 8 9 ) 10 } 11

Slide 61

Slide 61 text

3. Lambdas Option 1: function reference val viewModel = viewModel { MoviesViewModel() } onButtonClick = viewModel::onAddMovie, onMovieClick = viewModel::onMovieClick, @Composable 1 fun MoviesScreen() { 2 3 val state by viewModel.state.collectAsState() 4 5 MoviesListWithButton( 6 movies = state.movies, 7 8 9 ) 10 } 11

Slide 62

Slide 62 text

3. Lambdas Option 2: Manual memoization val onAddMovie = remember(viewModel) {{ viewModel.onAddMovie() }} val onMovieClick = remember(viewModel) {{ viewModel.onMovieClick() }} onButtonClick = onAddMovie, onMovieClick = onMovieClick, 1 2 3 MoviesListWithButton( 4 movies = state.movies, 5 6 7 ) 8

Slide 63

Slide 63 text

4. LazyColumn LazyColumn { items(movies) { MovieItem(movie = it) } }

Slide 64

Slide 64 text

4. LazyColumn items(movies, key = { it.id }) { LazyColumn { 1 2 MovieItem(movie = it) 3 } 4 } 5

Slide 65

Slide 65 text

5. Column Column { for (movie in movies) { MovieItem(movie) } }

Slide 66

Slide 66 text

5. Column key(movie.id) { } Column { 1 for (movie in movies) { 2 3 MovieItem(movie) 4 5 } 6 } 7

Slide 67

Slide 67 text

6. State continously changing val showScrollToTop = lazyState.firstVisibleItemIndex > 0

Slide 68

Slide 68 text

6. State continously changing val showScrollToTop = lazyState.firstVisibleItemIndex > 0 👇 val showScrollToTop by remember { derivedStateOf { lazyState.firstVisibleItemIndex > 0 } }

Slide 69

Slide 69 text

7. Defer reads from composition to layout var sliderValue by remember { mutableFloatStateOf(0f) } Column { Slider(value = sliderValue, onValueChange = { sliderValue = it }) RedBall(offset = sliderValue * 200) }

Slide 70

Slide 70 text

7. Defer reads from composition to layout RedBall(offset = sliderValue * 200) var sliderValue by remember { mutableFloatStateOf(0f) } 1 Column { 2 Slider(value = sliderValue, onValueChange = { sliderValue = it }) 3 4 } 5

Slide 71

Slide 71 text

7. Defer reads from composition to layout fun RedBall(offset: Float) { .offset(offset.dp) @Composable 1 2 Box( 3 modifier = Modifier 4 5 .size(56.dp) 6 ) 7 } 8

Slide 72

Slide 72 text

7. Defer reads from composition to layout fun RedBall(offset: () -> Float) { .offset { IntOffset(offset().toInt(), 0) } @Composable 1 2 Box( 3 modifier = Modifier 4 5 .size(56.dp) 6 ) 7 } 8

Slide 73

Slide 73 text

7. Defer reads from composition to layout RedBall(offset = { sliderValue * 200 })

Slide 74

Slide 74 text

What about the future?

Slide 75

Slide 75 text

What about the future? 💪 Strong Skipping Mode

Slide 76

Slide 76 text

Strong Skipping Mode Skippable Composables with unstable parameters Unstable parameters 👉 Compared by reference (===) Stable parameters 👉 Compared by equals() (==) All lambdas are memoized (remember) Even in unstable scopes

Slide 77

Slide 77 text

Strong Skipping Mode Requirements: Compose Compiler: 1.5.4+ Compose Runtime: =1.7.0+= (currently in beta) Requires opt-in

Slide 78

Slide 78 text

TLDR; Don't do premature optimization If there's a performance problem, debug: Layout inspector Stability report Future: use new recomposition system

Slide 79

Slide 79 text

Bibliography (1/2) 📄 (Android Developers) 📄 (Android Developers) 📄 (Android Developers) 📄 (Phat Nguyen) 📄 (Vinay Gaba) 📄 (Justin Breitfeller) 📄 (Ben Trengrove) 📄 (Francesc Vilarino) Thinking in Compose Lifecycle of Compose Jetpack Compose Performance Debugging the recomposition in Jetpack Compose What is “donut-hole skipping” in Jetpack Compose? Gotchas in Jetpack Compose Recomposition Jetpack Compose: Strong Skipping Mode Explained Exploring Jetpack Compose Compiler’s Stability Config

Slide 80

Slide 80 text

Bibliography (2/2) 📄 (Ben Trengrove) 💬 (Leland Richardson) 🎥 (Android Developers 🎥 (Android Developers) 🎥 (Philipp Lackner) 🎥 (Philipp Lackner) Jetpack Compose Stability Explained Reddit answer about unstable lambdas Jetpack Compose: Debugging recomposition From data to UI: Compose phases - MAD Skills Performance Optimization with @Stable and @Immutable in Jetpack Compose I Bet You DON'T Know These 3 Performance Optimizations for your Jetpack Compose UI

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

No content

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

No content