๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/Nutri Capture

Nutri Capture ํ”„๋ก ํŠธ์—”๋“œ - item ์ถ”๊ฐ€ใ†์ œ๊ฑฐ ์• ๋‹ˆ๋ฉ”์ด์…˜

interfacer_han 2024. 12. 15. 14:04

#1 ์• ๋‹ˆ๋ฉ”์ด์…˜

LazyColumn์— ์•„์ดํ…œ์ด ์ถ”๊ฐ€ใ†์ œ๊ฑฐ๋  ๋•Œ์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๊ณ  ํ•œ๋‹ค. ์ด ๋•Œ, ๊ทธ ๊ตฌํ˜„ ๋ฐฉ์‹์€ (๋‹น์žฅ ๋‚ด๊ฐ€ ๋ณด๊ธฐ์—) ์•„๋ž˜์™€ ๊ฐ™์ด 2๊ฐœ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค. 2๊ฐœ์˜ ๊ณต์‹ ๋ฌธ์„œ ๋งํฌ๋ฅผ ๋‹ฌ๊ฒ ๋‹ค. ๊ฐ๊ฐ์€ ์„œ๋กœ ๋‹ค๋ฅธ ๊ตฌํ˜„ ๋ฐฉ์‹์„ ์•Œ๋ ค์ฃผ๊ณ  ์žˆ๋‹ค.

 

#1-1 'Composable'์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ฐ€์ด๋“œ

 

์• ๋‹ˆ๋ฉ”์ด์…˜ ์ˆ˜์ •์ž ๋ฐ ์ปดํฌ์ €๋ธ”  |  Jetpack Compose  |  Android Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ˆ˜์ •์ž ๋ฐ ์ปดํฌ์ €๋ธ” ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. Compose์—๋Š” ์ผ๋ฐ˜์ ์ธ

developer.android.com

๊ฐ€์ด๋“œ์— ๊ธฐ์ˆ ๋œ ์ฝ”๋“œ์˜ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

์ฃผ์„ "your composable here" ๋ถ€๋ถ„์— ์ปดํฌ์ €๋ธ” ํ•จ์ˆ˜๋ฅผ ๋„ฃ์œผ๋ฉด, ํ•ด๋‹น ์ปดํฌ์ €๋ธ” ํ•จ์ˆ˜๊ฐ€ ํ™”๋ฉด์— ๋ณด์—ฌ์งˆ ๋•Œ(= visible ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’์ดtrue๋กœ ๋ณ€๊ฒฝ๋  ๋•Œ) ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๊ฐ€ ์ฒจ๊ฐ€๋œ๋‹ค. AnimatedVisibility()์™€ ๋น„์Šทํ•œ ๋™์ž‘ ๋ฐฉ์‹์„ ์ง€๋‹Œ AnimatedContent() ๋ฐ Crossfade()๋„ ์žˆ๋‹ค.

 

#1-2 'LazyColumn ๋‚ด item'์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ฐ€์ด๋“œ

 

๋ชฉ๋ก ๋ฐ ๊ทธ๋ฆฌ๋“œ  |  Jetpack Compose  |  Android Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชฉ๋ก ๋ฐ ๊ทธ๋ฆฌ๋“œ ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. ๋งŽ์€ ์•ฑ์—์„œ ํ•ญ๋ชฉ์˜ ์ปฌ๋ ‰์…˜์„ ํ‘œ์‹œํ•ด

developer.android.com

๊ฐ€์ด๋“œ์— ๊ธฐ์ˆ ๋œ ์ฝ”๋“œ์˜ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

Modifier์— animateItem()์„ ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ํ•œ๋‹ค. #1-1๋‚˜ #1-2๋‚˜ Jetpack Compose๋‹ต๊ฒŒ ๊ฐ„ํŽธํ•ด์„œ ์ข‹๋‹ค.

 

#1-3 ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๊ฒฐ์ •

#1-1์˜ ์ฝ”๋“œ๋ฅผ #1-2์˜ ๊ฒฝ์šฐ์—๋„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, #1-2์—์„œ ๋ณ„๋„์˜ ๋ฐฉ์‹์„ ๊ธฐ์ˆ ํ–ˆ๋‹ค๋Š” ์ ์—์„œ ์–ด๋–ค ์ด์œ ๊ฐ€ ์žˆ์„ ๊ฒƒ์œผ๋กœ ์ถ”์ธกํ•œ๋‹ค. ๋‹จ์ง€ ๊ทธ ์ด์œ ๋งŒ์œผ๋กœ #1-2๋ฅผ ์„ ํƒํ•  ์ด์œ ๋Š” ์ถฉ๋ถ„ํ•˜๊ธด ํ•˜๋‹ค. ๊ณต์‹์ด ์‹œํ‚ค๋Š” ๋Œ€๋กœ ํ•˜๋Š” ๊ฒŒ ์›ฌ๋งŒํ•˜๋ฉด ์˜ณ๋‹ค. ๋ฌผ๋ก  ๊ทธ ์„ ํƒ์˜ ์ด์œ ๋ฅผ ์•„๋ฌด๊ฑฐ๋‚˜ ํ•˜๋‚˜ ๊ณฑ์”น์–ด๋ณผ ์ˆ˜๋„ ์žˆ๊ฒ ๋‹ค. ๋ˆˆ์— ๋ณด์ด๋Š” ์ด์œ ๋„ ์žˆ๊ณ . ๊ทธ๊ฑด ๋ฐ”๋กœ ํ›„์ž์˜ ๊ฐ€์ด๋“œ๋Š” item ๋ชฉ๋ก์— ์ผ๊ด„์ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๋˜ ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ ๋ฐฉ์‹์ด๋ผ๋Š” ์ ์—์„œ, ์žฌ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์œ ์—ฐํ•˜๊ฒŒ ์ ์šฉํ•˜๊ธฐ ํŽธํ•  ๊ฒƒ์ด๋‹ค.

 

#2 ์ฝ”๋“œ - items()๋กœ ์ „ํ™˜

#2-1 key

๋ฐ”๋กœ animateItem()์„ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹๊ฒ ์ง€๋งŒ, ๋ฌธ์„œ์— "You should also provide a key via LazyListScope.item/LazyListScope.items for this modifier to enable animations. (์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด, ์ด Modifier์—๊ฒŒ LazyListScope.item/LazyListScope.items์„ ํ†ตํ•ด key๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.)"๋ผ๋Š” ๋ฌธ๊ตฌ๊ฐ€ ๋ณด์ธ๋‹ค. ์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” key๋Š”, ๊ตฌ์ฒด์ ์œผ๋กœ item() ๋˜๋Š” items()์˜ key ํ”„๋กœํผํ‹ฐ๋ฅผ ์˜๋ฏธํ•œ๋‹ค. item() ๋˜๋Š” items()์— ์ž„์˜์˜ key๋ฅผ ํ• ๋‹นํ•ด๋†“์œผ๋ฉด ๋‚˜๋จธ์ง€๋Š” ์•”์‹œ์ ์œผ๋กœ ์ˆ˜ํ–‰๋œ๋‹ค.

 

์‹ค์ œ๋กœ, key๊ฐ’์„ ์ œ๊ณตํ•˜์ง€์•Š์€ ์ƒํƒœ๋กœ animateItem()๋„ ์‚ฌ์šฉํ•ด๋ดค๋Š”๋ฐ ์—ญ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š์•˜๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์–ด๋–ค ์•„์ดํ…œ์— ์ ์šฉํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š”๊ฐ€?๋ฅผ ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ Compose Runtime์—๊ฒŒ ์•Œ๋ ค์ค˜์•ผํ•˜๋Š”๋ฐ, ๊ทธ๋Ÿฌ์งˆ ๋ชปํ–ˆ์œผ๋‹ˆ.

 

์•„๋ž˜ ์ฝ”๋“œ๋Š” key์˜ ๋ฐ์ดํ„ฐํ˜•์„ ๋ณผ ์ˆ˜ ์žˆ๋Š” LazyListScope.items() ํ•จ์ˆ˜์˜ ๋ชจ์–‘์ด๋‹ค.

open fun items(
    count: Int,
    key: ((index: Int) -> Any)? = null,
    contentType: (index: Int) -> Any? = { null },
    itemContent: @Composable LazyItemScope.(index: Int) -> Unit
): Unit

 

#2-2 ๋ฌด์—‡์„ key๋กœ ์จ์•ผ ํ•˜๋Š”๊ฐ€?

๋‚ด ํ”„๋กœ์ ํŠธ์—์„œ ๊ตฌํ˜„ํ•  ์• ๋‹ˆ๋ฉ”์ด์…˜์€, ๋‚ฑ๊ฐœ์˜ ์•„์ดํ…œ์— ๊ฐ๊ฐ ๋…๋ฆฝ์ ์œผ๋กœ ์ ์šฉ๋˜์–ด์•ผ ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ key๋Š” ๊ณ ์œ ํ•ด์•ผ ํ•œ๋‹ค. ๋˜, ์•„์ดํ…œ์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ ˆ์ฝ”๋“œ ํ•˜๋‚˜์— 1:1 ๋Œ€์‘๋˜๋ฏ€๋กœ ๋ ˆ์ฝ”๋“œ์˜ id๋ฅผ ๊ทธ๋Œ€๋กœ key๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋  ๊ฒƒ์ด๋‹ค.

 

#2-3 key ํ”„๋กœํผํ‹ฐ ๋ถ€์—ฌ

// in NutrientScreen.kt

...

@Composable
fun NutrientScreen(
    ...
) {
    ...

    LazyColumn(
        ...
    ) {
        item {
            ...
        }

        ...
        itemsIndexed(dayMeals, key = { _, dayMeal -> dayMeal.mealId }) { index, dayMeal ->
            Card(
                ...
            ) {
                ...
            }
        }
    }
}

๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋ฉด LazyListScope.itemsIndexed()๋Š” ์ด์ œ ํ•„์š”์—†๋‹ค. ๋ ˆ์ฝ”๋“œ๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š” key๊ฐ’์ด ์ƒ๊ฒผ์œผ๋‹ˆ ์—ญํ• ์ด ์ค‘๋ณต๋˜๋Š” ๊ฒƒ์ด๋‹ค. itemIndexed()๋ฅผ items()๋กœ ๋ฐ”๊พผ๋‹ค.

 

#2-4 items()๋กœ ์ „ํ™˜

// in NutrientScreen.kt

...

@Composable
fun NutrientScreen(
   ...
) {
    ...

    LazyColumn(
        ...
    ) {
        item {
            ...
        }

        ...
        items(dayMeals, key = { it.mealId }) { dayMeal ->
            val key = remember { dayMeal.mealId }

            Card(
                ...
            ) {
                ...
            }
        }
    }
}

items()๋กœ ์ „ํ™˜ํ•œ ์ฝ”๋“œ๋‹ค. key = ... ๋ถ€๋ถ„์˜ ๋žŒ๋‹ค ํ‘œํ˜„์‹ ๋ฌธ๋ฒ•์ด ๊ฐ„์†Œํ™”๋๋‹ค. itemsIndexed()์˜ key ํ”„๋กœํผํ‹ฐ ํ˜•์‹์€ ((index: Int, item) -> Any)?์ด๊ณ , items()๋Š” ((index: Int) -> Any)?๋‹ค. ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ 2๊ฐœ์—์„œ 1๊ฐœ๋กœ ์ค„์—ˆ์œผ๋ฏ€๋กœ, it ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ์œผ๋ฉฐ ๊ทธ๋ž˜์„œ ์‹œ๊ฐ์ ์œผ๋กœ ๊ฐ€๋ฒผ์›Œ์กŒ๋‹ค.

 

๋˜, (๋‚ด ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ) items() content ๋ถ€๋ถ„ ๋กœ์ง ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด key๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์žˆ๊ธฐ์— ๋ณ„๋„์˜ ๋กœ์ปฌ ํ”„๋กœํผํ‹ฐ๋„ ์„ ์–ธํ•ด์ฃผ์—ˆ๋‹ค.

 

#3 ์ฝ”๋“œ - ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ

#3-1 animateItem()

// in NutrientScreen.kt

@Composable
fun NutrientScreen(
   ...
) {
    ...

    LazyColumn(
        ...
    ) {
        item {
            ...
        }

        ...
        items(dayMeals, key = { it.mealId }) { dayMeal ->
            val key = remember { dayMeal.mealId }

            Card(
                modifier = Modifier
                    ...
                    .padding(
                        ...
                        top = if (key == dayMeals.last().mealId) 8.dp else 0.dp,
                        ...
                    )
                    .animateItem(),
                ...
            ) {
                Box(
                    ...
                ) {
                    Column(
                        ...
                    ) {
                        Text(
                            ...
                        )

                        Text(
                            text = "mealId: ${dayMeal.mealId}",
                            ...
                        )
                    }

                    IconButton(
                        ...
                    ) {
                        ...
                    }
                }
            }
        }
    }
}

animateItem()์„ Card์˜ Modifier์— ๋ฉ”์†Œ๋“œ ์ฒด์ด๋‹ํ•œ๋‹ค. ๋˜, index๊ฐ€ ์‚ฌ๋ผ์กŒ์œผ๋ฏ€๋กœ index์— ์˜์กดํ•˜๋˜ ๋‹ค๋ฅธ ์ฝ”๋“œ๋“ค๋„ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ”๊ฟ”์ค€๋‹ค.

 

#3-2 animateItem() ์ปค์Šคํ…€

open fun Modifier.animateItem(
    fadeInSpec: FiniteAnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
    placementSpec: FiniteAnimationSpec<IntOffset>? = spring(
                stiffness = Spring.StiffnessMediumLow,
                visibilityThreshold = IntOffset.VisibilityThreshold
            ),
    fadeOutSpec: FiniteAnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow)
): Modifier

์œ„ ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š” ๊ณต์‹ ๋ฌธ์„œ์˜ ๋งํฌ. #3-1์—์„œ๋Š” animateItem() 3๊ฐ€์ง€ ํ”„๋กœํผํ‹ฐ์—” ๊ธฐ๋ณธ๊ฐ’์ธ spring(stiffness = Spring.StiffnessMediumLow), spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = IntOffset.VisibilityThreshold), spring(stiffness = Spring.StiffnessMediumLow)์ด ๊ฐ๊ฐ ์ ์šฉ๋˜์—ˆ๋‹ค.

 

์„ธ ํ”„๋กœํผํ‹ฐ ์ „๋ถ€ FiniteAnimationSpec ๋ฐ์ดํ„ฐํ˜•์ธ๋ฐ, ์—ฌ๊ธฐ์— ํ• ๋‹นํ• ๋งŒํ•œ ํด๋ž˜์Šค๋Š” SpringSpec์™€ TweenSpec์ด ์žˆ๋‹ค. SpringSpec์—์„œ Spring์€ ์šฐ๋ฆฌ๊ฐ€ ์•„๋Š” ๊ทธ ์Šคํ”„๋ง(์šฉ์ˆ˜์ฒ )์„ ์˜๋ฏธํ•œ๋‹ค. ์šฉ์ˆ˜์ฒ ์ด ํŠ•๊ธฐ๋Š” ์ •๋„๊ฐ’(dampingRatio)๊ณผ ์šฉ์ˆ˜์ฒ ์˜ ๊ฐ•๋„(stiffness)๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์กฐ์ ˆํ•œ๋‹ค. TweenSpec์€ ์ข€ SpringSpec์— ๋น„ํ•ด ์ข€ ๋” ์„ธ์„ธํ•˜๋‹ค(๋ช…๋ น์ ์ด๋‹ค). TweenSpec์€ ์‹œ๊ฐ„์— ๊ธฐ๋ฐ˜ํ•œ๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์™„๋ฃŒ๋˜๊ธฐ๊นŒ์ง€์˜ ์‹œ๊ฐ„(durationMillis), ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์ž‘ ์ „ ๋Œ€๊ธฐ ์‹œ๊ฐ„(delayMillis), ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์Šคํƒ€์ผ(์†๋„ ๊ณก์„ ) (easing)๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์กฐ์ ˆํ•œ๋‹ค.

 

ํ˜„์žฌ๋Š” ์šฐ์„  ๊ธฐ๋ณธ๊ฐ’์„ ์“ฐ๊ณ  ๋„˜์–ด๊ฐ€์ง€๋งŒ, ๋‚˜์ค‘์—๋ผ๋„ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์•ฑ์˜ ์ปจ์…‰์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•  ์ผ์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ ‡๊ฒŒ ๊ฐ„๋‹จํžˆ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์ปค์Šคํ…€์— ๋Œ€ํ•ด ๊ธฐ๋กํ•ด๋‘”๋‹ค. 

 

#4 ์™„์„ฑ๋œ ์•ฑ

#4-1 ์ž‘๋™ ์˜์ƒ

 

#4-2 ์ด ๊ฒŒ์‹œ๊ธ€ ์‹œ์ ์˜ Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com

 

#4-3 ๋ณธ ํ”„๋กœ์ ํŠธ์˜ ๊ฐ€์žฅ ์ตœ์‹  Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com