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

Optimizing UI in Jetpack Compose - engineerHub

Optimizing UI in Jetpack Compose - engineerHub

Presented in engineerHub online presentation. How to optimise UI performance while using Jetpack Compose.

Rivu Chakraborty

July 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 • Android Architect @ Viacom18 • Previously ◦ Byju’s ◦ Paytm ◦ Gojek ◦ Meesho • Author (wrote multiple Kotlin books) • Speaker • 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] 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 }, ) } } }
  8. 🌐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) } } }
  9. 🌐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) } }
  10. 🌐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 )
  11. 🌐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())
  12. 🌐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.
  13. 🌐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(), ) { ... } } }
  14. 🌐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, ) } } }
  15. 🌐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.
  16. 🌐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) ) }
  17. 🌐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() } } ... }
  18. 🌐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 }
  19. 🌐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() } ... }
  20. 🌐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() } ) } ... }
  21. 🌐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.
  22. 🌐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.
  23. 🌐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) )
  24. 🌐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() } ) } ... }