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

Optimising UI in Jetpack Compose | Android Worldwide October 23

Optimising UI in Jetpack Compose | Android Worldwide October 23

Jetpack Compose is now really popular for UI development in Android. However, declarative UI is still a very new and a nuanced topic among Android developers.
Most of the developers are confused about how to write an optimized UI with Jetpack Compose, and lots of developers make mistakes on state management, passing parameters, and recomposing leading to having a slower, non-performant app on production. On the other hand, few developers fall into the rabbit hole of over-optimizing recomposition.
In this talk, we'll see how to improve the performance of your UI in Jetpack Compose, we'll learn about various APIs provided by the Jetpack Compose team to reduce unnecessary recomposition, we'll also learn handling lists in Compose and lastly, we'll learn about static analysis tools with Jetpack Compose, how to use them and some best practices.
This talk will cover
- Gap Buffer and Composer
- What's Composable
- What's Recomposition, how to optimise for it, and how not to over optimise for it
- Breaking down Composables
- Side Effects
- How remember works
- Working with Lists - kotlinx.immutablecollections
- suggestions on optimised state management
- @Stable and @Immutable annotations

Rivu Chakraborty

October 24, 2023
Tweet

More Decks by Rivu Chakraborty

Other Decks in Technology

Transcript

  1. • 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? 󰞦
  2. 🌐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() }
  3. 🌐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") } } }
  4. 🌐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() }
  5. 🌐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.
  6. 🌐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) } }
  7. 🌐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) } }, ) }
  8. 🌐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) } }, ) }
  9. 🌐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) } }, ) }
  10. 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] An Overloaded Composable Function @Composable fun

    ProfileScreenOverloaded( profileData: ProfileData, postsList: List<Post>, 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 }, ) } } }
  11. 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] Breaking it down @Composable fun ProfileScreen(

    profileData: ProfileData, postsList: List<Post>, modifier: Modifier = Modifier ) { LazyVerticalGrid( modifier = modifier, columns = GridCells.Fixed(2) ) { item(span = { GridItemSpan(2) }) { ProfileSection(profileData) } items(postsList) { PostItem(post = it) } } }
  12. 🌐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) } }
  13. 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @[email protected] What’s State? data class MoviesState( val

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

    query: String = emptyString(), val movies: List<Movie> = listOf(), val error: Throwable? = null, val isLoading: Boolean = false, val detail: MovieDetail? = null, val searchHistory: List<String> = emptyList(), val skipSplash: Boolean = false ) val moviesState: MoviesState by stateLiveData.observeAsState(initialState())
  15. 🌐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.
  16. 🌐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(), ) { ... } } }
  17. 🌐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, ) } } }
  18. 🌐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.
  19. 🌐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) ) }
  20. 🌐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() } } ... }
  21. 🌐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 }
  22. 🌐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
  23. 🌐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 -> "..." }
  24. 🌐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() } ... }
  25. 🌐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() } ) } ... }
  26. 🌐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.
  27. 🌐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.
  28. 🌐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) )
  29. 🌐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() } ) } ... }
  30. 🌐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