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

Optimising UI in Jetpack Compose | Devfest New Delhi 2023

Optimising UI in Jetpack Compose | Devfest New Delhi 2023

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

Jetpack Compose Course by Rivu: https://courses.rivu.dev/courses/jetpack-compose/

Rivu Chakraborty

October 07, 2023
Tweet

More Decks by Rivu Chakraborty

Other Decks in Technology

Transcript

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

    View full-size slide

  2. ● India’s first GDE (Google Developer Expert) for
    Kotlin
    ● More than decade in the Industry
    ● Android 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? 󰞦

    View full-size slide

  3. What’s @Composable

    View full-size slide

  4. 🌐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

    View full-size slide

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

    View full-size slide

  6. 🌐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()
    }

    View full-size slide

  7. 🌐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")
    }
    }
    }

    View full-size slide

  8. 🌐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()
    }

    View full-size slide

  9. 🌐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.

    View full-size slide

  10. Recomposition

    View full-size slide

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

    View full-size slide

  12. 🌐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)
    }
    }

    View full-size slide

  13. Breaking Down
    Composables

    View full-size slide

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

    View full-size slide

  15. 🌐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
    },
    )
    }
    }
    }

    View full-size slide

  16. 🌐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)
    }
    }
    }

    View full-size slide

  17. 🌐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)
    }
    }

    View full-size slide

  18. State Hoisting

    View full-size slide

  19. 🌐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
    )

    View full-size slide

  20. 🌐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())

    View full-size slide

  21. 🌐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.

    View full-size slide

  22. 🌐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(),
    ) {
    ...
    }
    }
    }

    View full-size slide

  23. 🌐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,
    )
    }
    }
    }

    View full-size slide

  24. Side Effects and Where
    to use Them

    View full-size slide

  25. 🌐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.

    View full-size slide

  26. 🌐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)
    )
    }

    View full-size slide

  27. 🌐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()
    }
    }
    ...
    }

    View full-size slide

  28. 🌐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
    }

    View full-size slide

  29. 🌐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

    View full-size slide

  30. 🌐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 -> "..."
    }

    View full-size slide

  31. 🌐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()
    }
    ...
    }

    View full-size slide

  32. 🌐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()
    }
    )
    }
    ...
    }

    View full-size slide

  33. @Stable and @Immutable

    View full-size slide

  34. 🌐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.

    View full-size slide

  35. 🌐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.

    View full-size slide

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

    View full-size slide

  37. 🌐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)
    )

    View full-size slide

  38. ImmutableList

    View full-size slide

  39. 🌐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()
    }
    )
    }
    ...
    }

    View full-size slide

  40. Checking Performance

    View full-size slide

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

    View full-size slide

  42. 🌐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

    View full-size slide

  43. 🌐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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. 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

    View full-size slide

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

    View full-size slide