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

Nutri Capture ํ”„๋ก ํŠธ์—”๋“œ - NutrientScreen ๊ตฌ์กฐ ์žก๊ธฐ

interfacer_han 2024. 10. 4. 17:20

#1 ๊นจ์•Œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ

#1-1 "NutrientInputScreen"์„ "NutrientScreen"์œผ๋กœ ๋ณ€๊ฒฝ

๊ฐ€๋…์„ฑ์„ ์œ„ํ•œ ์ด๋ฆ„ ๋ณ€๊ฒฝ์ด๋‹ค.
 

#1-2 "nutrient" ํŒจํ‚ค์ง€ ์ƒ์„ฑ

๋จผ์ €, "NutrientScreen"๋ฅผ "nutrient" ํŒจํ‚ค์ง€์— ๋„ฃ๋Š”๋‹ค. ๋˜, ๋ณธ ๊ฒŒ์‹œ๊ธ€์—์„œ ์ƒˆ๋กœ ๋งŒ๋“ค ๋ชจ๋“  ํŒŒ์ผ์€ ํ•ด๋‹น ํŒจํ‚ค์ง€์— ๋“ค์–ด๊ฐ„๋‹ค
 

#2 ๊ฐœ์š”

#2-1 ๊ฐ์ฒด ์ง€ํ–ฅ์  UI ์„ค๊ณ„

 

[Android] Jetpack Compose - ๊ฐ์ฒด ์ง€ํ–ฅ์  UI ๋ ˆ์ด์–ด ์„ค๊ณ„

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

kenel.tistory.com

์œ„ ๊ฒŒ์‹œ๊ธ€์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์งฐ๋‹ค.
 

#2-2 ๋งŒ๋“ค ํŒŒ์ผ

1. (View์˜ ์—ญํ• ) nutrient.NutrientScreen
2. (Event์˜ ์—ญํ• ) nutrient.NutrientScreenEvent
3. (State์˜ ์—ญํ• ) nutrient.NutrientScreenState
4. (ViewModel์˜ ์—ญํ• ) nutrient.NutrientViewModel
5. (Event์˜ ์—ญํ• ) nutrient.NutrientViewModelEvent
6. (๋ฐ์ดํ„ฐ ํด๋ž˜์Šค) nutrient.NutritionInfo

1. NutrientScreen
๊ธฐ์กด์— ์žˆ๋˜ ํŒŒ์ผ์ด๋‹ค. View์˜ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
 
2. NutrientScreenEvent
ViewModel์—์„œ View์—๊ฒŒ ์š”์ฒญํ•˜๋Š” ์ด๋ฒคํŠธ๋‹ค. ํ•ด๋‹น ์ด๋ฒคํŠธ์˜ ์ธ์Šคํ„ด์Šค๋Š” ViewModel์— Flow๋กœ ์ •์˜ํ•˜๋ฉฐ, ํ•ด๋‹น Flow๋ฅผ View์—์„œ collect()ํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค (์ฐธ์กฐ: ์ด ๊ฒŒ์‹œ๊ธ€์˜ #4).
 
3. NutrientScreenState
View๊ฐ€ ๋ณด์œ ํ•  ์ •๋ณด๋ฅผ ์ •์˜ํ•œ๋‹ค. ์ธ์Šคํ„ด์Šค๋Š” ViewModel์— ์„ ์–ธ๋œ๋‹ค.
 
4. NutrientViewModel
View๋ฅผ ๋ณด์กฐํ•˜๋Š” ViewModel์ด๋‹ค.
 
5. NutrientViewModelEvent
View์—์„œ ViewModel์—๊ฒŒ ์š”์ฒญํ•˜๋Š” ์ด๋ฒคํŠธ๋‹ค. ์ธ์Šคํ„ด์Šค๋Š” ๊ทธ๋•Œ ๊ทธ๋•Œ ๋งŒ๋“ค์–ด ์“ด๋‹ค. ์™œ๋ƒํ•˜๋ฉด NutrientScreenEvent์ฒ˜๋Ÿผ collect()๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. View์—์„œ ViewModel์˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉฐ NutrientViewModelEvent๋ฅผ ์ „๋‹ฌํ•  ๊ฒƒ์ด๋‹ค.
 
6. NutritionInfo
์ด ๊ฒŒ์‹œ๊ธ€์˜ #2-4์—์„œ ๋งํ–ˆ๋˜ '๊ฐœ์ธํ™”๋œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ'๋ฅผ ์ €์žฅํ•  ๊ณณ์ด๋‹ค. ์—ฌ๊ธฐ์„œ ์„ธ๋ถ€ ์‚ฌํ•ญ์„ ๊ตฌํ˜„ํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค (ํ‹€๋งŒ ์žก์Œ).
 

#3 ์ฝ”๋“œ - NutrientViewModel

#3-1 NutrientViewModel.kt ์ƒ์„ฑ

// package com.example.nutri_capture_new.nutrient

import androidx.lifecycle.ViewModel

class NutrientViewModel : ViewModel() {
    
}

 

#3-2 Jetpack Compose์šฉ ViewModel ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‹ค์šด๋กœ๋“œ - ๋ชจ๋“ˆ ์ˆ˜์ค€ build.gradle

plugins {
    ...
}

android {
    ...
}

dependencies {

    ...

    // ViewModel (Compose)
    implementation(libs.androidx.lifecycle.viewmodel.compose)
}

์ด์ œ #2-1์—์„œ ๋งŒ๋“  ViewModel์„ ์‚ฌ์šฉํ•ด์•ผํ• ํ…๋ฐ, ๋ณธ ๊ฒŒ์‹œ๊ธ€์ฒ˜๋Ÿผ Jetpack Compose ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ๋ผ๋ฉด ViewModel์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•˜๋‚˜๊ฐ€ ์š”๊ตฌ๋œ๋‹ค. ํ˜น์‹œ ํ”„๋กœ์ ํŠธ์— libs.versions ํŒŒ์ผ์ด ์—†๋‹ค๋ฉด implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")๋ผ๊ณ  ์ž…๋ ฅํ•˜๋ฉด ๋œ๋‹ค.
 

#3-3 Jetpack Compose์šฉ ViewModel ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋‹ค์šด๋กœ๋“œ - libs.versions

[versions]
...

[libraries]
...
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
...

[plugins]
...

 

#3-4 View์— ViewModel ์ฃผ์ž… (์ƒ์„ฑ์ž ์ฃผ์ž…)

// package com.example.nutri_capture_new.nutrient

...
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun NutrientScreen(
    scope: CoroutineScope,
    snackbarHostState: SnackbarHostState,
    viewModel: NutrientViewModel = viewModel<NutrientViewModel>()
) {
    ...
}

NutrientScreen์—์„œ ์ƒ์„ฑ์ž ์ธ์ˆ˜์— ViewModel๋ฅผ ์ฃผ์ž…ํ•œ๋‹ค.
 

#4 ์ฝ”๋“œ - NutrientScreenState

#4-1 NutrientScreenState.kt ์ƒ์„ฑ

// package com.example.nutri_capture_new.nutrient

import java.time.LocalDate
import java.time.LocalTime

data class NutrientScreenState(
    val dailyMeals: List<DailyMeal>
)

data class DailyMeal(
    val date: LocalDate,
    val meals: List<Meal>
)

data class Meal(
    val time: LocalTime,
    val name: String,
    val nutritionInfo: NutritionInfo,
)

val dailyMeals: List<DailyMeal>
View์˜ LazyColumn์—์„œ ํ‘œ์‹œํ•  ์•„์ดํ…œ๋“ค์˜ ๋ฆฌ์ŠคํŠธ๋‹ค.
 
data class DailyMeal
ํ•˜๋ฃจ์— ๋จน์€ ์‹์‚ฌ๋‚˜ ๊ฐ„์‹๋“ค์„ ๋ณด์œ ํ•˜๋Š” ํด๋ž˜์Šค๋‹ค.
 
data class Meal
์‹์‚ฌ๋‚˜ ๊ฐ„์‹์„ ๋จน์€ ์‹œ๊ฐ, ์ด๋ฆ„, ์˜์–‘ ์ •๋ณด๋ฅผ ๋ณด์œ ํ•œ๋‹ค. NutritionInfo ํด๋ž˜์Šค๋Š” ์•„๋ž˜์— ์žˆ๋‹ค.
 

#4-2 NutritionInfo.kt ์ƒ์„ฑ

// package com.example.nutri_capture_new.nutrient

data class NutritionInfo(
    val mealSize: Int
)

์„ธ๋ถ€ ๊ตฌํ˜„์€ ๋‚˜์ค‘์— ํ•œ๋‹ค. ์šฐ์„ ์€ ์–ผ๋งˆ๋‚˜ ๋จน์—ˆ๋Š”์ง€(๊ณผ์‹ ์ •๋„)๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๋ณ€์ˆ˜ ํ•˜๋‚˜๋งŒ ๋ฉ๊ทธ๋Ÿฌ๋‹ˆ ๋„ฃ์–ด๋‘์—ˆ๋‹ค.
 

#4-3 ViewModel์— NutrientScreenState๋ฅผ ๋‹ด๋Š” State ๋ณ€์ˆ˜ ์„ ์–ธ

...

class NutrientViewModel : ViewModel() {
    // (1) ํ™”๋ฉด ํ‘œ์‹œ์šฉ State
    private val _nutrientScreenState = mutableStateOf(
        NutrientScreenState(
            dailyMeals = emptyList()
        )
    )
    val nutrientScreenState: State<NutrientScreenState>
        get() = _nutrientScreenState
}

NutrientScreenState๋Š” ์ด๋ฆ„์— "State"๊ฐ€ ๋“ค์–ด๊ฐ€์ง€๋งŒ ์ง„์งœ State๋Š” ์•„๋‹ˆ๋‹ค. NutrientScreenState ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ mutableStateOf( ... )์— ๋„ฃ์Œ์œผ๋กœ์จ ์ง„์งœ State๋กœ ๋งŒ๋“ ๋‹ค.
 

#5 ์ฝ”๋“œ - NutrientScreenEvent

#5-1 NutrientScreenEvent.kt ์ƒ์„ฑ

// package com.example.nutri_capture_new.nutrient

sealed class NutrientScreenEvent {
    data class ShowSnackbar(val message: String) : NutrientScreenEvent()
}

์Šค๋‚ต๋ฐ”๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์ด๋ฒคํŠธ ํ•˜๋‚˜๋ฅผ ์ž์‹ ์ด๋ฒคํŠธ๋กœ ์ •์˜ํ–ˆ๋‹ค. ๋‹ค๋ฅธ ์ด๋ฒคํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๋‚˜์ค‘์— ์ถ”๊ฐ€ํ•˜๊ฒ ๋‹ค.
 

#5-2 ViewModel์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜๋Š” Flow ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

...

class NutrientViewModel : ViewModel() {
    // (1) ํ™”๋ฉด ํ‘œ์‹œ์šฉ State
    ...

    // (2) View์—์„œ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•  ์ด๋ฒคํŠธ
    private val _nutrientScreenEventFlow = MutableSharedFlow<NutrientScreenEvent>()
    val nutrientScreenEventFlow: SharedFlow<NutrientScreenEvent>
        get() = _nutrientScreenEventFlow.asSharedFlow()
}

ViewModel์— ์ด๋ฒคํŠธ๋ฅผ ๋‹ด๋Š” SharedFlow๋ฅผ ์„ ์–ธํ•œ๋‹ค.
 

#5-3 View์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜๋Š” Flow ์ธ์Šคํ„ด์Šค collect()

...

@Composable
fun NutrientScreen(
    scope: CoroutineScope,
    snackbarHostState: SnackbarHostState,
    viewModel: NutrientViewModel = viewModel<NutrientViewModel>()
) {
    LaunchedEffect(key1 = true) {
        // ViewModel๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
        viewModel.nutrientScreenEventFlow.collectLatest { event ->
            when (event) {
                is NutrientScreenEvent.ShowSnackbar -> {
                    scope.launch {
                        snackbarHostState.showSnackbar(
                            message = event.message,
                            duration = SnackbarDuration.Short
                        )
                    }
                }
            }
        }
    }
}

ViewModel์˜ SharedFlow๋ฅผ collect()ํ•˜๊ณ  ํ•ด๋‹น Flow ์ˆ˜์‹  ์‹œ์˜ ๋™์ž‘์„ ์ •์˜ํ•œ๋‹ค.
 

#6 ์ฝ”๋“œ - NutrientViewModelEvent

#6-1 NutrientViewModelEvent.kt ์ƒ์„ฑ

// package com.example.nutri_capture_new.nutrient

sealed class NutrientViewModelEvent {
    data object InitializeState : NutrientViewModelEvent()
}

State๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ด๋ฒคํŠธ ํ•˜๋‚˜๋ฅผ ์ž์‹ ์ด๋ฒคํŠธ๋กœ ์ •์˜ํ–ˆ๋‹ค. ๋‹ค๋ฅธ ์ด๋ฒคํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ๋‚˜์ค‘์— ์ถ”๊ฐ€ํ•œ๋‹ค.
 

#6-2 ViewModel์—์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜ onEvent() ์„ ์–ธ

...

class NutrientViewModel : ViewModel() {
    // (1) ํ™”๋ฉด ํ‘œ์‹œ์šฉ State
    ...

    // (2) View์—์„œ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•  ์ด๋ฒคํŠธ
    ...

    // (3) View๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
    fun onEvent(event: NutrientViewModelEvent) {
        when (event) {
            is NutrientViewModelEvent.InitializeState -> {
                _nutrientScreenState.value = _nutrientScreenState.value.copy(
                    dailyMeals = listOf(
                        DailyMeal(
                            date = LocalDate.of(2011, 11, 11),
                            meals = emptyList()
                        ),
                        DailyMeal(
                            date = LocalDate.of(2011, 11, 12),
                            meals = emptyList()
                        ),
                        DailyMeal(
                            date = LocalDate.of(2011, 11, 13),
                            meals = emptyList()
                        ),
                        DailyMeal(
                            date = LocalDate.of(2011, 11, 14),
                            meals = emptyList()
                        ),
                        DailyMeal(
                            date = LocalDate.of(2011, 11, 15),
                            meals = emptyList()
                        )
                    )
                )
            }
        }
    }
}

๋”๋ฏธ์šฉ์œผ๋กœ, 2011๋…„ 11์›” 11์ผ๋ถ€ํ„ฐ 15์ผ๊นŒ์ง€์˜ DailMeal ์ธ์Šคํ„ด์Šค 5๊ฐœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ฒŒ๋” ๋‘๊ฒ ๋‹ค.
 

#6-3 View์—์„œ State๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ด๋ฒคํŠธ ViewModel์— ๋ณด๋‚ด๊ธฐ

...
@Composable
fun NutrientScreen(
    scope: CoroutineScope,
    snackbarHostState: SnackbarHostState,
    viewModel: NutrientViewModel = viewModel<NutrientViewModel>()
) {
    LaunchedEffect(key1 = true) {
        // State ์ดˆ๊ธฐํ™”
        viewModel.onEvent(NutrientViewModelEvent.InitializeState)

        // ViewModel๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
        ...
    }
}

'State ์ดˆ๊ธฐํ™”' ์ฝ”๋“œ๋Š” 'ViewModel๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ' ๋ณด๋‹ค LaunchedEfffect { ... } ๋‚ด์—์„œ ๋จผ์ € ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด 'State ์ดˆ๊ธฐํ™”' ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค. ์•„๋งˆ collectLastest()๊ฐ€ suspend ํ‚ค์›Œ๋“œ๊ฐ€ ๋ถ™๋Š” ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ผ์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋กœ ๋ณด์ธ๋‹ค.
 

#6-4 ๊ฐ„๋‹จํ•œ LazyColumn ์„ ์–ธ

...

@Composable
fun NutrientScreen(
    scope: CoroutineScope,
    snackbarHostState: SnackbarHostState,
    viewModel: NutrientViewModel = viewModel<NutrientViewModel>()
) {
    LaunchedEffect(key1 = true) {
        ...
    }

    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        val dailyMeals = viewModel.nutrientScreenState.value.dailyMeals
        items(dailyMeals) { dailyMeal ->
            Text(
                text = dailyMeal.date.toString(),
                color = Color.White,
                fontSize = 40.sp
            )
        }
    }
}

์ง™์€ ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ LazyColumn์— ํ•˜์–€ ๊ธ€์”จ item์ด ๋‚˜์˜ค๋Š” ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋‹ค. #5-2์—์„œ ๋งŒ๋“ค์–ด๋‘์—ˆ๋˜ ๋”๋ฏธ DailyMeal ์ธ์Šคํ„ด์Šค 5๊ฐœ๊ฐ€ ๋ณด์—ฌ์•ผ ํ•  ๊ฒƒ์ด๋‹ค. #7-1์— ํ•ด๋‹น ํ™”๋ฉด ์Šคํฌ๋ฆฐ์ƒท์ด ์žˆ๋‹ค.
 

#7 ์š”์•ฝ

NutrientScreen ๊ตฌํ˜„์„ ์œ„ํ•œ ํฐ ํ‹€์„ ๊ฐ์ฒด ์ง€ํ–ฅ์ ์ธ ๋ฐฉ์‹์œผ๋กœ ์žก์•„๋‘์—ˆ๋‹ค.
 

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

#8-1 ์Šคํฌ๋ฆฐ์ƒท

 

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

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com

 

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

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com