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

Debugging Jetpack Compose

Debugging Jetpack Compose

2023년 7월 14일 코엑스 인터콘티넨탈 파르나스에서 열린 <2023 대한민국 인공지능위크> 모바일 트랙에서 발표한 내용입니다. 2023 Google I/O에서 공개된 동명의 세션을 기반으로 Jetpack Compose 디버깅 도구와 사례를 소개합니다.

[링크]
Google I/O 세션: https://www.youtube.com/watch?v=Kp-aiSU8qCU
블로그 : https://workspace-dev.medium.com
HOLIX Jetpack Compose 사용자 모임 : https://hlx.kr/workspace

workspace

July 14, 2023
Tweet

More Decks by workspace

Other Decks in Programming

Transcript

  1. Debugging Jetpack Compose
    Kimin Ryu,
    Senior Developer, HOLIX

    View Slide

  2. View Slide

  3. Agenda
    • Jetpack Compose Debugging Tools
    • Debugging Practice
    • Debugging Mindset
    • Common Issues
    • recomposition
    • jank
    • tracing
    • Tips
    • Summary

    View Slide

  4. Jetpack Compose Debugging Tools
    Compose ѐߊ द ࢎਊೞח ٣ߡӦ بҳٜ

    View Slide

  5. 1. Layout Inspector
    ઱۽ ޙઁܳ ੋ૑ ೞחؘ ࢎਊ


    • Recomposition Count


    • Recomposition Skip Count


    • Recomposition Highlight


    * Android Studio Hedgehogࠗఠ


    э਷ చীࢲ Device ചݶҗ Layout Inspectorܳ ೣԋ ࢎਊೡ ࣻ ੓਺ (Embedded Layout Inspector)

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. “৵ ੉ composableীࢲ
    recomposition੉ ੌযզө?”

    View Slide

  11. Types of
    recomposition
    Unskippable
    recomposition
    Indirect
    recomposition
    A parent composable got recomposed
    and this caused parameters to change
    Direct
    recomposition
    A compose state object inside the
    composable changed.

    View Slide

  12. @Composable
    fun DirectRecomposition() {
    var count by remember { mutableStateOf(0) }
    Text("$count")
    Button(onClick = { count++ }) {
    Text("Increment")
    }
    }

    View Slide

  13. @Composable
    fun IndirectRecomposition() {
    var count by remember { mutableStateOf(0) }
    val doubled = count * 2
    MyText(doubled)
    Button(onClick = { count++ }) {
    Text("Increment")
    }
    }
    @Composable
    fun MyText(count: Int) {
    Text("$count")
    }

    View Slide

  14. @Composable
    fun UnstableRecomposition() {
    var count by remember { mutableStateOf(0) }
    Text("$count")
    var list by remember { mutableStateOf(listOf(1, 2, 3)) }
    MyList(list)
    Button(onClick = { count++ }) {
    Text("Increment")
    }
    }
    @Composable
    fun MyList(list: List) { /**/ }

    View Slide

  15. Problem:
    Composableীࢲ যڃ ч ٸޙী
    recomposition੉ ੌযաח૑ ঌ ࣻ হ਺

    View Slide

  16. 2. Recomposition state in debugger🎉

    View Slide

  17. Recomposition state
    • Unchanged - ߸҃੉ হ਺


    • Changed - ߸҃੉ ੌযթ


    • Uncertain


    • Static - ߸҃੉ হਸ Ѫ੉ۄ ౸ױؽ


    • Unstable

    View Slide

  18. @Composable
    fun IndirectRecomposition() {
    var count by remember { mutableStateOf(0) }
    val doubled = count * 2
    MyText(doubled)
    Button(onClick = { count++ }) {
    Text("Increment")
    }
    }
    @Composable
    fun MyText(count: Int) {
    Text("$count")
    }

    View Slide

  19. View Slide

  20. @Composable
    fun UnstableRecomposition() {
    var count by remember { mutableStateOf(0) }
    Text("$count")
    var list by remember { mutableStateOf(listOf(1, 2, 3)) }
    MyList(list)
    Button(onClick = { count++ }) {
    Text("Increment")
    }
    }
    @Composable
    fun MyList(list: List) { /**/ }

    View Slide

  21. View Slide

  22. 3. Logs
    ੣਷ Recomposition੉ ߊࢤೞח ࢚ടীࢲ જ਷ ؀উ


    झ௼܀੉ա গפݫ੉࣌੉ ੌযզ ٸ


    ч੉ ࡅܰѱ ߸ೞݴ recomposeغחؘ,


    breakpointܳ ੟ই ݒ ೐ۨ੐݃׮ чਸ ࠁח Ѫ਷ য۰਑

    View Slide

  23. @Composable
    fun ScrollingList() {
    val scrollState = rememberLazyListState()
    var count by remember {
    mutableStateOf(0)
    }
    Log.d("ScrollingList", "${scrollState.firstVisibleItemIndex}")
    Button(onClick = { count++ }) {
    Text(text = "Increment")
    }
    Text(text = "count: $count")
    LazyColumn(
    modifier = Modifier.fillMaxSize(),
    state = scrollState,
    ) {
    items(100) {
    Text(text = "index $it")
    }
    }
    }

    View Slide

  24. View Slide

  25. @Composable
    fun ScrollingList() {
    val scrollState = rememberLazyListState()
    var count by remember {
    mutableStateOf(0)
    }
    Log.d("ScrollingList", "${scrollState.firstVisibleItemIndex}")
    Button(onClick = { count++ }) {
    Text(text = "Increment")
    }
    Text(text = "count: $count")
    LazyColumn(
    modifier = Modifier.fillMaxSize(),
    state = scrollState,
    ) {
    items(100) {
    Text(text = "index $it")
    }
    }
    }

    View Slide

  26. @Composable
    fun ScrollingList() {
    val scrollState = rememberLazyListState()
    var count by remember {
    mutableStateOf(0)
    }
    SideEffect {
    Log.d("ScrollingList", "${scrollState.firstVisibleItemIndex}")
    }
    Button(onClick = { count++ }) {
    Text(text = "Increment")
    }
    Text(text = "count: $count")
    LazyColumn(
    modifier = Modifier.fillMaxSize(),
    state = scrollState,
    ) {
    items(100) {
    Text(text = "index $it")
    }
    }
    }

    View Slide

  27. View Slide

  28. 4. Visual Lint
    22֙ I/Oীࢲ ୭ୡ ҕѐ ׼दী View दझమ݅ ੸ਊ.


    Android Studio Hedgehogীࢲ


    Compose Previewী ؀ೠ ૑ਗ द੘🎉

    View Slide

  29. View Slide

  30. View Slide

  31. 5. Tracing (Profiler)
    System trace
    ծ਷ overhead
    दр ஏ੿੉ ਊ੉ೣ
    ݃ఊػ ੉߮౟݅ਸ ୶੸
    Method trace
    ֫਷ overhead
    ݽٚ method callਸ ࠁৈષ

    View Slide

  32. View Slide

  33. View Slide

  34. View Slide

  35. “System trace੄ ࡅܲ ࣘب৬
    Method trace੄ ੿ࠁܳ о઎ਵݶ…”

    View Slide

  36. View Slide

  37. // build.gradle
    dependencies {
    implementation(“androidx.compose.runtime:runtime-tracing:")
    }

    View Slide

  38. View Slide

  39. 6. Macrobenchmark
    ইې৬ э਷ য೒ܻா੉࣌ ز੘੄ ࢿמਸ ஏ੿ೡ ࣻ ੓਺
    • App startup
    • Scroll
    • Animation

    View Slide

  40. View Slide

  41. Debugging Practice
    ٣ߡӦ ೐۽ࣁझ ୓೷ ೧ࠁӝ

    View Slide

  42. Debugging Mindset
    A process for debugging

    View Slide

  43. Define Reproduce
    Validate
    Assumptions
    Fix

    View Slide

  44. ٣ߡӦ਷ ޙઁо ޖ঺ੋ૑ ੿੄ೞח Ѫীࢲ द੘


    • যڌѱ ز੘ೞח Ѫ੉ۄ ৘࢚೮ա?


    • ؀न যڃ ੌ੉ ੌযաҊ ੓ա?


    • ৵ Ӓۧѱ ز੘ೡ Ѫ੉ۄ ࢤп೮ա?


    • ׼नҗ زܐٜ਷ যڃ о੿ਸ ೞҊ ੓ա?
    Define Reproduce
    Validate
    Assumptions
    Fix
    What is the problem?

    View Slide

  45. ੤അ оמೠ ৘दܳ ҳഅ


    • ৘࢚чҗ पઁч, ഑਷ ৘࢚ ز੘җ पઁ ز੘ਸ ࠺Үೞח పझ౟ ੘ࢿ


    • (TIP) ੘਷ playground ೐۽ં౟ ഑਷ ݽٕਸ ٜ݅যفݶ ಞೞ׮.
    Define Reproduce
    Validate
    Assumptions
    Fix
    Reproduce the problem

    View Slide

  46. ޙઁܳ ੤അೡ ࣻ ੓ਵפ بҳܳ ੉ਊೞৈ оࢸਸ Ѩૐ


    • ޙઁী ٮۄ ب਑ਸ ߉ਸ ࣻ ੓ח ׮নೠ بҳٜ੉ ੓਺
    Define Reproduce
    Validate
    Assumptions
    Fix
    Validate Assumptions

    View Slide

  47. ޙઁܳ ೧Ѿ🎉


    • పझ౟ܳ ా೧ ޙઁо ೧Ѿغ঻ח૑ ഛੋ
    Define Reproduce
    Validate
    Assumptions
    Fix
    Implement the fix

    View Slide

  48. Debugging Case #1
    Recomposition
    Layout Inspector۽ recompositionਸ ನ଱ೞҊ, recomposition stateܳ ഛੋ೧ ޙઁ ೧Ѿ੄ द੘੼ ଺ӝ

    View Slide

  49. View Slide

  50. View Slide

  51. lastEpisodeDate is unstable!

    View Slide

  52. FollowedPodcastCarouselItem(

    lastEpisodeDate = lastEpisodeDate,

    )
    @Composable
    private fun FollowedPodcastCarouselItem(

    lastEpisodeDate: OffsetDateTime? = null,

    ) {

    if (lastEpisodeDate != null) {
    Text(
    text = lastEpisodeDate?.let { lastUpdated(it) },

    )
    }

    }

    View Slide

  53. FollowedPodcastCarouselItem(

    lastEpisodeDateText = lastEpisodeDate?.let { lastUpdated(it) },

    )
    @Composable
    private fun FollowedPodcastCarouselItem(

    lastEpisodeDateText: String? = null,

    ) {

    if (lastEpisodeDateText != null) {
    Text(
    text = lastEpisodeDateText,

    )
    }

    }

    View Slide

  54. Debugging Case #2
    Jank
    Scroll, Animation١ীࢲ ߡߢ੐੉ ߊࢤ೮ਸ ٸ, ޙઁܳ ೧ѾೞҊ MacroBenchmark۽ ѐࢶ ഛੋೞӝ

    View Slide

  55. View Slide

  56. @Google I/O 23

    View Slide

  57. Debugging Case #3
    Tracing
    दрਸ য়ې ੟ইݡח Composable੉ ੓ח Ѫ э׮? Profiler - System traceܳ ੉ਊ

    View Slide

  58. View Slide

  59. View Slide

  60. View Slide

  61. x

    View Slide

  62. class BigObject {
    init {
    List(250000) { it }
    }
    }

    View Slide

  63. class BigObject {
    init {
    trace("Big Object") {
    List(250000) { it }
    }
    }
    }

    View Slide

  64. View Slide

  65. val expensive = remember { BigObject() }

    View Slide

  66. fun ProvideAndroidCompositionLocals(
    owner: AndroidComposeView,
    content: @Composable () -> Unit
    ) {
    val view = owner
    val context = view.context
    ……
    val uriHandler = remember { AndroidUriHandler(context) }
    val saveableStateRegistry = remember {
    DisposableSaveableStateRegistry(view, viewTreeOwners.savedStateRegistryOwner)
    }
    ……
    val imageVectorCache = obtainImageVectorCache(context, configuration)
    }

    View Slide

  67. Tip: Twitter Jetpack Compose rule
    Jetpack Composeীࢲ पࣻೞѢա ֬஖ӝ ए਍ ࠗ࠙ਸ ੟ই઱ח ktlint rules


    View Slide

  68. View Slide

  69. View Slide

  70. Tip: Compose Compiler Reports
    Unstable class৬ parameterܳ ޷ܻ ଺ח ߨ


    View Slide

  71. Setup
    // build.gradle (root)
    subprojects {
    tasks.withType().configureEach {
    compilerOptions {
    if (project.findProperty("composeCompilerReports") == "true") {
    freeCompilerArgs.addAll(
    "-P",
    "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
    project.buildDir.absolutePath + “/compose_compiler”,
    )
    }
    if (project.findProperty("composeCompilerMetrics") == "true") {
    freeCompilerArgs.addAll(
    "-P",
    "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
    project.buildDir.absolutePath + “/compose_compiler”
    )
    }
    }
    }
    }

    View Slide

  72. Build
    # 1. local.properties, gradle.properties ١ীࢲ property ࢸ੿


    composeCompilerReports=true
    composeCompilerMetrics=true
    # 2. terminalীࢲ gradle task प೯ द property ࢸ੿
    ➜ ./gradlew -PcomposeCompilerReports=true -PcomposeCompilerMetrics=true assembleDebug

    View Slide

  73. Result

    View Slide

  74. Top Level Metrics (-module.json)
    {
    "skippableComposables": 16,
    "restartableComposables": 20,
    "readonlyComposables": 0,
    "totalComposables": 20,
    "restartGroups": 20,
    "totalGroups": 20,
    "staticArguments": 21,
    "certainArguments": 1,
    "knownStableArguments": 276,
    "knownUnstableArguments": 1,
    "unknownStableArguments": 0,
    "totalArguments": 277,
    "markedStableClasses": 0,
    "inferredStableClasses": 3,
    "inferredUnstableClasses": 2,
    "inferredUncertainClasses": 0,
    "effectivelyStableClasses": 3,
    "totalClasses": 5,
    "memoizedLambdas": 15,
    "singletonLambdas": 1,
    "singletonComposableLambdas": 9,
    "composableLambdas": 9,
    "totalLambdas": 15
    }

    View Slide

  75. Top Level Metrics (-module.json)
    {
    "skippableComposables": 16,
    "restartableComposables": 20,
    “knownUnstableArguments": 1,
    }

    View Slide

  76. Composable Signatures (-composables.txt)
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun Greeting(
    stable name: String
    stable modifier: Modifier? = @static Companion
    )
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun GreetingPreview()
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun DirectRecomposition()
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun IndirectRecomposition()
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MyText(
    stable count: Int
    )
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ScrollingList()
    restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun UnstableRecomposition()
    restartable scheme("[androidx.compose.ui.UiComposable]") fun MyList(
    unstable list: List
    )

    View Slide

  77. Composable Signatures (-composables.txt)
    restartable scheme("[androidx.compose.ui.UiComposable]") fun MyList(
    unstable list: List
    )

    View Slide

  78. Composable Table (-composables.csv)
    package,name,composable,skippable,restartable,readonly,inline,isLambda,hasDefaults,defau
    ltsGroup,groups,calls,
    com.example.sample.Greeting,Greeting,1,1,1,0,0,0,0,0,1,1,
    com.example.sample.GreetingPreview,GreetingPreview,1,1,1,0,0,0,0,0,1,1,
    com.example.sample.DirectRecomposition,DirectRecomposition,1,1,1,0,0,0,0,0,1,4,
    com.example.sample.IndirectRecomposition,IndirectRecomposition,1,1,1,0,0,0,0,0,1,4,
    com.example.sample.MyText,MyText,1,1,1,0,0,0,0,0,1,1,
    com.example.sample.ScrollingList,ScrollingList,1,1,1,0,0,0,0,0,1,8,
    com.example.sample.UnstableRecomposition,UnstableRecomposition,1,1,1,0,0,0,0,0,1,1,
    com.example.sample.MyList,MyList,1,0,1,0,0,0,0,0,1,1,

    View Slide

  79. classes.txt
    data class StableClass(
    val stableProperty1: String,
    val stableProperty2: Int,
    val stableProperty3: Long,
    val stableProperty4: Double
    )
    data class StableNestedClass(
    val stableClass: StableClass
    )
    data class UnstableClass(
    val stableProperty1: String,
    val stableProperty2: Int,
    val stableProperty3: Long,
    val unstableProperty1: List, // Date, Map, Set...
    )
    data class UnstableNestedClass(
    val unstableClass: UnstableClass
    )
    stable class StableClass {
    stable val stableProperty1: String
    stable val stableProperty2: Int
    stable val stableProperty3: Long
    stable val stableProperty4: Double
    = Stable
    }
    stable class StableNestedClass {
    stable val stableClass: StableClass
    = Stable
    }
    unstable class UnstableClass {
    stable val stableProperty1: String
    stable val stableProperty2: Int
    stable val stableProperty3: Long
    unstable val unstableProperty1: List
    = Unstable
    }
    unstable class UnstableNestedClass {
    unstable val unstableClass: UnstableClass
    = Unstable
    }

    View Slide

  80. ⚠ ݣ౭ ݽٕਸ ॳҊ ҅द׮ݶ?
    ݣ౭ ݽٕ ೐۽ં౟ীࢲ domainҗ э਷ ࣽࣻ kotlin module੄ ௿ېझח

    Compose compilerо unstable۽ р઱


    3rd party ۄ੉࠳۞ܻܳ ࢎਊೡ ٸب ਬ੄೧ঠ…


    1. Compose runtime dependencyܳ ୶оೞѢա


    2. UI moduleীࢲ ui model۽ mapping ژח wrapper۽ ੜ хऱࢲ ࢎਊ

    View Slide

  81. ݽٚ Composable਷ Skippable੉যঠ?
    ইפ׮.


    skippable۽ ݅٘ח ࣻҊо प૕੸ੋ о஖ࠁ׮ ੸਷ ҃਋ ޖद


    • ੗઱ recomposeо غ૑ ঋח composable (৘: ചݶ ױਤ)


    • ױࣽ൤ ղࠗীࢲ skippableೠ composableਸ о૑Ҋ ੓ח ҃਋


    View Slide

  82. Compose Compiler Report to HTML

    View Slide

  83. View Slide

  84. View Slide

  85. View Slide

  86. Mendable

    View Slide

  87. Summary

    View Slide

  88. 1. ޙઁ ߊࢤ ߑ૑ೞӝ
    • Lint۽ ੜޅػ ز੘ਸ ঠӝೞח ௏٘ܳ ੘ࢿೞ૑ ঋب۾ ೞ੗


    • Twitter compose rules


    • Visual Lint


    • Compose Compiler Reportsܳ ా೧ ਫ਼੤੸ਵ۽ ޙઁо ੓ח class, composableਸ ଺੗.

    View Slide

  89. ز੘ী ޙઁо ੓ਸ ٸ ؀୊ߨ
    • Layout Inspectorܳ ా೧ ࠛ೙ਃೠ recompositionਸ ଺ইࠁ੗.


    • Composable functionী breakpointܳ Ѧয ٣ߡѢীࢲ Recomposition Stateܳ ഛੋೡ ࣻ ੓׮.


    • झ௼܀, গפݫ੉࣌ ١ ࠼بо ੣਷ recomposition੉ ߊࢤೡ ٸח Logܳ ࢎਊೞ੗.

    View Slide

  90. ୭੸ച
    • composable tracingਸ ా೧ Composableীࢲ য়ې Ѧܻח ࠗ࠙ਸ ఐ૑ೞӝ


    • Macrobenchmarkܳ ా೧ ࢿמ ೱ࢚ ഛੋ

    View Slide

  91. Resources
    • https://goo.gle/jetcaster-pager


    • https://goo.gle/compose-stability-explained


    • https://goo.gle/baseline-profiles


    • https://goo.gle/compose-tracing


    • https://medium.com/androiddevelopers/jetpack-compose-debugging-recomposition-bfcf4a6f8d37


    • https://medium.com/androiddevelopers/jetpack-compose-composition-tracing-9ec2b3aea535


    • https://youtube.com/watch?v=Kp-aiSU8qCU

    View Slide

  92. Q&A
    Q. Composable ௏٘ ܻ࠭ ౲੉ ੓ਸөਃ?


    A. ୊਺ী ٣੗ੋ दझమਸ উ੟Ҋ ௏٘ܳ ૢ ٸח ઺ࠂغח ஹನք౟ب ݆Ҋ modifierо ؋૑؋૑ ׳ܻחѱ ࠁӝب
    উજওणפ׮. ஹನ੷࠶ ௏٘о ݠ݁ࣘীࢲ ੜ উӒ۰૑ח ҃਋о ݆ওחؘ ٣੗ੋ दझమ ҳഅਸ ೞݶࢲ оةࢿ੉ ѐ
    ࢶغ঻؍ ҃೷੉ ੓णפ׮.


    ௏٘ ܻ࠭ ౲਷ ইק Ѫ э૑݅, ઁ ҃೷ਸ ݈ॹܻ٘੗ݶ ҳഅ ࢎ೦ਸ যڌѱ ҳഅೡ૑ ҳ࢚ਸ alignೞח োणਸ ૓೯೮
    णפ׮. Composeח ࢜۽਍ ӝࣿ੉׮ࠁפ п੗ ѐੋٜ੄ ࣼ۲ب৬ ૑ध Ѻରо ߥয૕ ࣻ ੓חؘ, ੷ח ಕয ೐۽Ӓ
    ې߁, VOD, ࢎղ ࣁ޷աܳ ా೧ ࢜۽਍ ૑धਸ ੹౵೮णפ׮. ੉۠ җ੿ਸ ా೧ ௏٘ ܻ࠭ ੉੹ী ࢎҊߑधਸ ੌ஖द
    ெ ௏٘ ܻ࠭ ബਯਸ ֫ੌ ࣻ ੓঻णפ׮.

    View Slide

  93. Q&A
    Q. Compose internals ъ੄? ଼?ਸ ࠁݶ Compose Compilerо ঌইࢲ ೧઻ࢲ @Stable, @Immutable
    annotationਸ ׮ח Ѫਸ ௼ѱ न҃ ॳ૑ ঋইب ػ׮Ҋ غয੓חؘ যڌѱ ࢤпೞदաਃ? (૕ޙ ӝরաח؀۽ ੸যࠌ
    חؘ ݏѷભ…?)


    A. Ӓ ޙ੢ ੹റ۽ যڃ ղਊ੉ ੓ח૑ ઁо ੍যࠁ૑ ঋইࢲ ૒੽੸ਵ۽ ׹ਸ ೧ܻ٘ӟ য۰਎ Ѫ эणפ׮. ׮݅,
    @Stableҗ @Immutable੄ ৉ೡҗ ӝמਸ ੿ഛ൤ ইन׮ݶ ੷ח ԙ ೙ਃೞ׮Ҋ ࢤп೤פ׮. @Stableҗ
    @Immutable annotation੄ ഝਊ਷ ৉द Compose ղࠗ ௏٘ܳ ଵઑೞदݶ જणפ׮.


    Color, Dp৬ э਷ Ѫٜ ࠗఠ द੘ೞৈ material, material3١ componentٜী ٜযоח ੋ੗ٜ੄ ҳഅਸ ా೧
    annotation੄ ৉ೡਸ ഛੋ ೞप ࣻ ੓ਸ Ѫ эणפ׮. ੷ח ৮੹ ୡӝী ੗ܐо ցޖ ࠗ઒ೞ؍ द੺ ଵҊೡ݅ೠѱ ղࠗ
    ௏٘ ߆ী হ঻חؘ, ૑Әب stabilityী ؀ೠ ӝળਸ ࣁ਋ח ਬബೠ ೟णߑध੉ۄ ࢤп೤פ׮.

    View Slide

  94. Thank you

    View Slide