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 @rivu@androiddev.social Who am I? 󰞦
  2. 🌐https://www.rivu.dev/ youtube.com/@rivutalks @rivuchakraborty @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social 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 @rivu@androiddev.social @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 @rivu@androiddev.social @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 @rivu@androiddev.social @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 @rivu@androiddev.social 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() } ) } ... }