Slide 1

Slide 1 text

Optimizing UI in Jetpack Compose Rivu Chakraborty 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] https://courses.rivu.dev/ AndroidWorldwide

Slide 2

Slide 2 text

● India’s first GDE (Google Developer Expert) for Kotlin ● More than decade in the Industry ● Mobile Architect @ JioCinema ● Previously ○ Byju’s ○ Paytm ○ Gojek ○ Meesho ● Author (wrote multiple Kotlin books) ● Speaker ● Mentor ● Community Person ● YouTuber (?) 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Who am I? 󰞦

Slide 3

Slide 3 text

What’s @Composable

Slide 4

Slide 4 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s @Composable? 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Not an annotation processor

Slide 5

Slide 5 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s @Composable? Not an annotation processor Works similar to suspend keyword

Slide 6

Slide 6 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s @Composable? @Composable fun BlueText() { Text( text = "https://www.youtube.com/@rivutalks", color = Color.Blue ) } fun BlueText($composer: Composer) { $composer.start(123) Text( text = "https://www.youtube.com/@rivutalks", color = Color.Blue ) $composer.end() }

Slide 7

Slide 7 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s @Composable? @Composable fun RecompositionScreen(viewModel: RecompositionViewModel) { val recomposeState = viewModel.state Column { Text(text = "Recomposition count ${recomposeState.count}") Button(onClick = { viewModel.recompose() }) { Text(text = "Recompose") } } }

Slide 8

Slide 8 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s @Composable? fun RecompositionScreen($composer: Composer, viewModel: MyViewModel) { $composer.start(123) val recomposeState = viewModel.state Column { Text(text = "Recomposition count ${recomposeState.count}") Button(onClick = { viewModel.recompose() }) { Text(text = "Recompose") } } $composer.end() }

Slide 9

Slide 9 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s Composer? A calling context object, responsible for rendering (composing/recomposing) Composables. The implementation contains a data-structure closely related to Gap Buffer.

Slide 10

Slide 10 text

Recomposition

Slide 11

Slide 11 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s Recomposition Recursion with updated parameters

Slide 12

Slide 12 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s Recomposition fun BlueText($composer: Composer) { $composer.start(123) Text( text = "https://www.youtube.com/@rivutalks", color = Color.Blue ) $composer.end()?.updateScope { nextComposer -> BlueText(nextComposer) } }

Slide 13

Slide 13 text

Phases of Compose

Slide 14

Slide 14 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] 3 Phases of Compose Composition Layout Drawing

Slide 15

Slide 15 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Composition https://developer.android.com/jetpack/compose/phases

Slide 16

Slide 16 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Composition https://developer.android.com/jetpack/compose/phases

Slide 17

Slide 17 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Layout https://developer.android.com/jetpack/compose/phases

Slide 18

Slide 18 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Drawing https://developer.android.com/jetpack/compose/phases

Slide 19

Slide 19 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Phased State Reads https://developer.android.com/jetpack/compose/phases

Slide 20

Slide 20 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Recomposition loop @Composable fun AutoResizeText( modifier: Modifier = Modifier, text: String, style: TextStyle, targetTextSize: TextUnit = style.fontSize, maxLines: Int = 1, ) { var textSize by remember { mutableStateOf(targetTextSize) } Text( modifier = modifier, text = text, fontSize = textSize, //... maxLines = maxLines, overflow = TextOverflow.Ellipsis, onTextLayout = { textLayoutResult -> val maxCurrentLineIndex: Int = textLayoutResult.lineCount - 1 if (textLayoutResult.isLineEllipsized(maxCurrentLineIndex)) { textSize = textSize.times(TEXT_SCALE_REDUCTION_INTERVAL) } }, ) }

Slide 21

Slide 21 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Recomposition loop @Composable fun AutoResizeText( modifier: Modifier = Modifier, text: String, style: TextStyle, targetTextSize: TextUnit = style.fontSize, maxLines: Int = 1, ) { var textSize by remember { mutableStateOf(targetTextSize) } Text( modifier = modifier, text = text, fontSize = textSize, //... maxLines = maxLines, overflow = TextOverflow.Ellipsis, onTextLayout = { textLayoutResult -> val maxCurrentLineIndex: Int = textLayoutResult.lineCount - 1 if (textLayoutResult.isLineEllipsized(maxCurrentLineIndex)) { textSize = textSize.times(TEXT_SCALE_REDUCTION_INTERVAL) } }, ) }

Slide 22

Slide 22 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Recomposition loop @Composable fun AutoResizeText( modifier: Modifier = Modifier, text: String, style: TextStyle, targetTextSize: TextUnit = style.fontSize, maxLines: Int = 1, ) { var textSize by remember { mutableStateOf(targetTextSize) } Text( modifier = modifier, text = text, fontSize = textSize, //... maxLines = maxLines, overflow = TextOverflow.Ellipsis, onTextLayout = { textLayoutResult -> val maxCurrentLineIndex: Int = textLayoutResult.lineCount - 1 if (textLayoutResult.isLineEllipsized(maxCurrentLineIndex)) { textSize = textSize.times(TEXT_SCALE_REDUCTION_INTERVAL) } }, ) }

Slide 23

Slide 23 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Recomposition loop

Slide 24

Slide 24 text

Breaking Down Composables

Slide 25

Slide 25 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] A Screen

Slide 26

Slide 26 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] An Overloaded Composable Function @Composable fun ProfileScreenOverloaded( profileData: ProfileData, postsList: List, modifier: Modifier = Modifier ) { LazyVerticalGrid( modifier = modifier, columns = GridCells.Fixed(2) ) { item(span = { GridItemSpan(2) }) { Column { AsyncImage( model = profileData.profileImageUrl, contentDescription = profileData.name, modifier = Modifier .size(128.dp) .align(Alignment.CenterHorizontally) .clickable { //Handle Profile Photo Click }, ) Text( text = profileData.name, style = MaterialTheme.typography.h2, modifier = Modifier .align(Alignment.CenterHorizontally), ) Text( text = profileData.location, style = MaterialTheme.typography.body1, modifier = Modifier .align(Alignment.CenterHorizontally), ) Button( onClick = { /*Follow Button Action*/ }, modifier = Modifier .align(Alignment.CenterHorizontally) .background(Color.Black), ) { Text( text = "Follow ${profileData.name}", style = MaterialTheme.typography.body2, ) } Button( onClick = { /*Message Button Action*/ }, modifier = Modifier .align(Alignment.CenterHorizontally) .border(1.dp, color = Color.Black, shape = RoundedCornerShape(5.dp)), ) { Text( text = "Message", style = MaterialTheme.typography.body1, ) } } } items(postsList) { AsyncImage( model = profileData.profileImageUrl, contentDescription = profileData.name, modifier = Modifier .width(167.dp).height(220.dp) .clickable { //Handle Post Photo Click }, ) } } }

Slide 27

Slide 27 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Breaking it down @Composable fun ProfileScreen( profileData: ProfileData, postsList: List, modifier: Modifier = Modifier ) { LazyVerticalGrid( modifier = modifier, columns = GridCells.Fixed(2) ) { item(span = { GridItemSpan(2) }) { ProfileSection(profileData) } items(postsList) { PostItem(post = it) } } }

Slide 28

Slide 28 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Breaking it down @Composable fun ProfileSection( profileData: ProfileData, modifier: Modifier = Modifier ) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { ProfilePhoto( profileImageUrl = profileData.profileImageUrl, contentDesc = profileData.name, profileId = profileData.profileId) Text( text = profileData.name, style = Typography.titleMedium, ) Text( text = profileData.location, style = Typography.bodyMedium, ) FollowButton( profileId = profileData.profileId, name = profileData.name) MessageButton(profileId = profileData.profileId) } }

Slide 29

Slide 29 text

State Hoisting

Slide 30

Slide 30 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s State? data class MoviesState( val query: String = emptyString(), val movies: List = listOf(), val error: Throwable? = null, val isLoading: Boolean = false, val detail: MovieDetail? = null, val searchHistory: List = emptyList(), val skipSplash: Boolean = false )

Slide 31

Slide 31 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s State? data class MoviesState( val query: String = emptyString(), val movies: List = listOf(), val error: Throwable? = null, val isLoading: Boolean = false, val detail: MovieDetail? = null, val searchHistory: List = emptyList(), val skipSplash: Boolean = false ) val moviesState: MoviesState by stateLiveData.observeAsState(initialState())

Slide 32

Slide 32 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] State Hoisting Having UI logic and UI element state in composables is a good approach if the state and logic are simple. You can leave your state internal to a composable or hoist as required.

Slide 33

Slide 33 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] State Hoisting @Composable fun ShowList( state: ListState.Content, modifier: Modifier = Modifier, onRefresh: () -> Unit = {}, ) { val swipeRefreshState = rememberPullRefreshState(refreshing = state.isRefreshing, onRefresh = onRefresh) Box(modifier = modifier.pullRefresh(swipeRefreshState)) { LazyColumn( modifier = Modifier.matchParentSize(), ) { ... } } }

Slide 34

Slide 34 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] State Hoisting fun FormScreen(viewModel: FormViewModel) { val state = viewModel.state var addItemQuery by remember { mutableStateOf("") } LaunchedEffect(key1 = addItemQuery) { ... } Column { Text(text = "Add form items below") if (state is MyFormState.Content) { MyForm( state = state, onAddItem = { details -> addItemQuery = details }, onUpdateItem = { position, item -> viewModel.updateItem(position, item) }, shouldShowAddItem = true, ) } } }

Slide 35

Slide 35 text

Side Effects and Where to use Them

Slide 36

Slide 36 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What are Side Effects? A side-effect is a change to the state of the app that happens outside the scope of a composable function. Due to composables' lifecycle and properties such as unpredictable recompositions, executing recompositions of composables in different orders, or recompositions that can be discarded, composables should ideally be side-effect free.

Slide 37

Slide 37 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] LaunchedEffect @Composable fun FormScreen(viewModel: FormViewModel) { val state = viewModel.state var addItemQuery by remember { mutableStateOf("") } LaunchedEffect(key1 = addItemQuery) { if (addItemQuery.isBlank()) return@LaunchedEffect delay(500) viewModel.addItem( MyFormItem(addItemQuery) ) }

Slide 38

Slide 38 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] DisposableEffect @Composable fun ListScreen(viewModel: ListViewModel) { val state = viewModel.myListState Log.d(LogTag, "ListScreen Composed") DisposableEffect(key1 = Unit) { val job = viewModel.loadItems() onDispose { job.cancel() } } ... }

Slide 39

Slide 39 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] SideEffect @Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }

Slide 40

Slide 40 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] LaunchedEffect val text = when (state.value) { is State.Loading -> "Loading" is State.NotLoggedIn -> { logout() "" } is State.Success -> {...} } Don’t Do

Slide 41

Slide 41 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] LaunchedEffect Do var isLogout by remember { mutableStateOf(state.value is State.NotLoggedIn) } LaunchedEffect(key1 = isLogout) { if (isLogout) { onLogout() } } val text = when (state.value) { is State.Loading -> "Loading" is State.NotLoggedIn -> "" is State.Success -> "..." }

Slide 42

Slide 42 text

remember

Slide 43

Slide 43 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] remember @Composable fun ShowList( state: ListState.Content, modifier: Modifier = Modifier, onRefresh: () -> Unit = {}, ) { val sortedItems = state.items.let { Log.d(LogTag, "Sorted") it.sortedBy { it.itemId }.toImmutableList() } ... }

Slide 44

Slide 44 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] remember @Composable fun ShowList( state: ListState.Content, modifier: Modifier = Modifier, onRefresh: () -> Unit = {}, ) { Log.d(LogTag, "List Composed isRefreshing ${state.isRefreshing}") val sortedItems by remember(state.items) { mutableStateOf( state.items.let { Log.d(LogTag, "Sorted") it.sortedBy { it.itemId }.toImmutableList() } ) } ... }

Slide 45

Slide 45 text

@Stable and @Immutable

Slide 46

Slide 46 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] @Stable Stable is used to communicate some guarantees to the compose compiler about how a certain type or function will behave.

Slide 47

Slide 47 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] @Immutable Immutable can be used to mark class as producing immutable instances. The immutability of the class is not validated and is a promise by the type that all publicly accessible properties and fields will not change after the instance is constructed. This is a stronger promise than val as it promises that the value will never change not only that values cannot be changed through a setter.

Slide 48

Slide 48 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] @Immutable @Immutable data class MyListItem( val itemId: Int, val itemColor: Color, val backgroundColor: Color )

Slide 49

Slide 49 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] @Stable @Stable data class MyListItem( val itemId: Int, val itemColor: Color, var backgroundColor: Color by mutableStateOf(Color.Blue) )

Slide 50

Slide 50 text

ImmutableList

Slide 51

Slide 51 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] ImmutableList @Composable fun ShowList( state: ListState.Content, modifier: Modifier = Modifier, onRefresh: () -> Unit = {}, ) { val sortedItems by remember(state.items) { mutableStateOf( state.items.let { Log.d(LogTag, "Sorted") it.sortedBy { it.itemId }.toImmutableList() } ) } ... }

Slide 52

Slide 52 text

Checking Performance

Slide 53

Slide 53 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Layout Inspector

Slide 54

Slide 54 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Compiler Metrics https://patilshreyas.github.io/compose -report-to-html/ Generate Compiler Metrics in HTML and Take Actions

Slide 55

Slide 55 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Generate Compiler Metrics plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id "dev.shreyaspatil.compose-compiler-report-gener ator" version "1.1.0" } ./gradlew :app:releaseComposeCompilerHtmlReport

Slide 56

Slide 56 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Analyse Compiler Metrics

Slide 57

Slide 57 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Analyse Compiler Metrics

Slide 58

Slide 58 text

🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Analyse Compiler Metrics

Slide 59

Slide 59 text

Resources ● https://developer.android.com/jetpack/compose/state ● https://github.com/androidx/androidx/blob/androidx-main/compose/docs/ compose-component-api-guidelines.md#last-updated-july-19-2023 ● https://youtube.com/@rivutalks ● https://courses.rivu.dev

Slide 60

Slide 60 text

Thank You Rivu Chakraborty 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] https://courses.rivu.dev/ AndroidWorldwide