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

Nutri Capture ๋ฐฑ์—”๋“œ - StateFlow๋กœ ์ „ํ™˜

interfacer_han 2025. 1. 2. 20:12

#1 ๊ฐœ์š”

#1-1 State์—์„œ StateFlow๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

 

[Kotlin] Coroutines Flow - StateFlow

#1 ๊ฐœ์š” StateFlowA SharedFlow that represents a read-only state with a single updatable data value that emits updates to the value to its collectors. A state flow is a hot flow because its active instance exists independently of the presence of collecto

kenel.tistory.com

์œ„ ๊ฒŒ์‹œ๊ธ€์„ ์ฐธ์กฐํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ–ˆ๋‹ค.

 

#1-2 StateFlow ์—…๋ฐ์ดํŠธ

์ด์ „ ๊ฒŒ์‹œ๊ธ€์˜ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ, ViewModel์˜ State ๋ณ€์ˆ˜๋ฅผ StateFlow๋กœ ๋ณ€๊ฒฝ(#1-1)ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Repository ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜ํ˜•์„ Flow๋กœ ๋ณ€๊ฒฝํ•œ ๋’ค,

 

// ๊ตฌ์กฐ๋ฅผ ๋ณด์ด๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋กœ, ์‹ค์ œ ์ฝ”๋“œ(#2)์™€๋Š” ์„ธ๋ถ€์ ์œผ๋กœ ๋‹ค๋ฆ„

repository.getAllItems().collect { itemList ->
    _items.value = itemList // Flow๋กœ๋ถ€ํ„ฐ ๊ฐ’์„ ๊ฐฑ์‹ ๋ฐ›๋Š” StateFlow ํ”„๋กœํผํ‹ฐ
}

์™€ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๋งŒ๋“ ๋‹ค. ์ด๋ ‡๊ฒŒ ๊ตฌ์กฐ๋ฅผ ์งœ ๋†“์œผ๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์–ด๋–ค ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๊ทธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค โ†’ DAO โ†’ Repository โ†’ ViewModel โ†’ View ์ˆœ์œผ๋กœ ์•”์‹œ์ ์œผ๋กœ ์ ์šฉ๋œ๋‹ค (View๋Š” ์ด๋ฏธ ViewModel์— ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ).

 

#1-3 ํ•œ๊ณ„์ ๊ณผ ๋Œ€์ฒ˜ ๋ฐฉ์•ˆ

๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ View๊นŒ์ง€ ์ญ‰ ์ด์–ด์ง„๋‹ค๋Š” ๊ฑด 1์ฐจ์ ์œผ๋กœ๋Š” ์ข‹์•„ ๋ณด์ธ๋‹ค. ํ•˜์ง€๋งŒ, ๋ณธ ํ”„๋กœ์ ํŠธ๋Š” UI์— Page ๊ฐœ๋…์ด ์ ์šฉ๋˜์–ด ์žˆ๋‹ค. ์ตœ์ดˆ์— ํŠน์ • ๊ฐฏ์ˆ˜์˜ ์š”์†Œ๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ์‚ฌ์šฉ์ž์˜ ์Šคํฌ๋กค์„ ๊ฐ์ง€ํ•  ๋•Œ๋งˆ๋‹ค ์ถ”๊ฐ€๋กœ ํ‘œ์‹œํ•  ์š”์†Œ๋ฅผ ๋ถˆ๋Ÿฌ๋“ค์ด๋Š” ๋ฐฉ์‹์ด๋‹ค (๋ฌดํ•œ ์Šคํฌ๋กค). ์ฆ‰ ์›๋ž˜์˜ ํ”„๋ก ํŠธ์—”๋“œ ๋กœ์ง์„ ์œ ์ง€ํ•˜๋ ค๋ฉด, (#1-2์˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ) ๋ชจ๋“  ์•„์ดํ…œ์„ ์ „๋ถ€ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐฉ์‹์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค๋Š” ์–˜๊ธฐ๋‹ค.

 

 

Nutri Capture ๋ฐฉํ–ฅ์„ฑ - ๊ฐœ๋ฐœ ์ผ์ •ํ‘œ (1์ฐจ)

#1 ํ˜„ ๊ฐœ๋ฐœ ํ–‰ํƒœ์— ๋Œ€ํ•œ ๋ฌธ์ œ์ #1-1 ๊ณผ์ •์˜ ์™„๋ฒฝ์ฃผ์˜๋‚œ ํ’‹๋‚ด๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋จธ์— ๋ถˆ๊ณผํ•˜๋‹ค. ๋‚ด๊ฐ€ ๋งŒ๋“ค ์•ฑ ๋˜ํ•œ ๊ทธ์ € ๊ทธ๋Ÿฐ ์•ฑ์ผ ๊ฒƒ์ด๋‹ค. ์ ์–ด๋„ ์ฒ˜์Œ (์ถœ์‹œํ•  ์˜๋„๋กœ ๋งŒ๋“œ๋Š”) ์•ฑ์„ ๋‹น์—ฐํžˆ ๊ทธ๋Ÿด ๊ฒƒ์ด๋‹ค,

kenel.tistory.com

๊ทธ๋Ÿผ์—๋„ (#1-2์˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ) ํ†ต์งธ๋กœ ๋ชจ๋“  ์•„์ดํ…œ์„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ๋กœ ํ•œ๋‹ค. ๊ทธ๊ฒŒ ์‰ฝ๊ณ  ๋น ๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ธฐ๊ป ๊ตฌํ˜„ํ•ด๋‘์—ˆ๋˜ ๋ฌดํ•œ ์Šคํฌ๋กค์€ ์ž ์‹œ ํฌ๊ธฐํ•˜๊ฒ ๋‹ค. ๊ฐœ๋ฐœ ์ผ์ •ํ‘œ๋ฅผ ์ง€์ผœ์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์œ„ ๊ฒŒ์‹œ๊ธ€์—์„œ, ์ผ๋‹จ ๋™์ž‘ํ•˜๋ฉด ๋„˜๊ธฐ๊ธฐ๋กœ ์Šค์Šค๋กœ์—๊ฒŒ ์•ฝ์†ํ–ˆ๋‹ค. ๊ทธ๋ž˜์•ผ ๊ณผ์ •์˜ ์™„๋ฒฝ์ฃผ์˜๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฒฐ๊ณผ์˜ ์™„๋ฒฝ์ฃผ์˜๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ. ๋‚˜์ค‘์— ํ•œ๊บผ๋ฒˆ์— ๋ฆฌํŒฉํ† ๋งํ•˜๊ฑฐ๋‚˜, ์‹œ๊ฐ„์ด ๋‚จ์•„๋Œ ๋•Œ ์กฐ๊ธˆ์”ฉ ์ƒ๊ฐํ•ด๋ณด๊ธฐ๋กœ ํ•˜๊ฒ ๋‹ค. ๊ทธ ํŽธ์ด ์‹œ๊ฐ„ ๋Œ€๋น„ ๋” ์ข‹์€ ์ฝ”๋“œ๊ฐ€ ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค (๊ฒฐ๊ณผ์˜ ์™„๋ฒฝ์ฃผ์˜).

 

์ด๋ฒˆ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์—์„œ ์ œ๊ฑฐํ•ด๋ฒ„๋ฆด ์ฝ”๋“œ๊ฐ€ ๊ฝค ๋  ๊ฒƒ์ด๋‹ค. ์–ด์ฐจํ”ผ ์ด์ „ ์ฝ”๋“œ๋“ค์€ Git(Hub)์— ๊ธฐ๋ก๋˜์–ด ์žˆ์œผ๋‹ˆ ๋ฌธ์ œ๋  ๊ฑด ์—†๋‹ค.

 

#2 ์ฝ”๋“œ

#2-1 NutrientViewModel.kt

...

class NutrientViewModel(private val repository: MainRepository) : ViewModel() {
    // (1) ํ™”๋ฉด ํ‘œ์‹œ์šฉ State
    private val _nutrientScreenState = MutableStateFlow(
        NutrientScreenState(
            dayMeals = SnapshotStateList()
        )
    )
    val nutrientScreenState: StateFlow<NutrientScreenState>
        get() = _nutrientScreenState

    init {
        viewModelScope.launch {
            repository.getAllDayMeals().collect { dayMeals ->
                _nutrientScreenState.value.dayMeals.apply {
                    clear()
                    addAll(dayMeals)
                }
            }
        }
    }

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

    // (3) View๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
    fun onEvent(event: NutrientViewModelEvent) {
        when (event) {
            is NutrientViewModelEvent.InsertMeal -> {
                viewModelScope.launch {
                    repository.insertMeal(event.meal, event.date)
                }
            }

            is NutrientViewModelEvent.DeleteMeal -> {
                viewModelScope.launch {
                    repository.deleteMeal(event.meal)
                }
            }

            is NutrientViewModelEvent.DeleteDayMeal -> {
                viewModelScope.launch {
                    repository.deleteDayMeal(event.dayMeal)
                }
            }
        }
    }
}

#1-2์—์„œ ๋งํ•œ ๊ฑธ init { ... } ๋ถ€๋ถ„์— ๊ตฌํ˜„ํ–ˆ๋‹ค. ๋˜, onEvent() ๋ถ€๋ถ„์ด ๋Œ€ํญ ๊ฐ„์†Œํ™”๋˜์—ˆ๋‹ค.

 

#2-2 MainRepository.kt

// package com.example.nutri_capture_new.db

import kotlinx.coroutines.flow.Flow
import java.time.LocalDate

class MainRepository(private val dao: MainDAO) {
    ...

    fun getAllDayMeals(): Flow<List<DayMealView>> {
        return dao.getAllDayMeals()
    }

    ...
}

LiveData๊ฐ€ ๋ฐ˜ํ™˜ํ˜•์ผ ๋•Œ์™€ ๊ฐ™์€ ์ด์œ ๋กœ, ๋ฐ˜ํ™˜ํ˜•์ด Flow์ผ ๋•Œ๋„ suspend ํ‚ค์›Œ๋“œ๋ฅผ ๋ถ™์ด์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ suspend ํ‚ค์›Œ๋“œ๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.

 

#2-3 MainDAO.kt

...

@Dao
interface MainDAO {
    ...

    @Query("""
    SELECT * FROM DayMealView
    ORDER BY day_date DESC,
             meal_time DESC,
             meal_id DESC
    """)
    fun getAllDayMeals(): Flow<List<DayMealView>>

    ...
}

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ฐ˜ํ™˜ํ˜•์„ ๋ฐ”๊พธ๊ณ , suspend ํ‚ค์›Œ๋“œ๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.

 

#2-4 NutrientViewModelEvent.kt

package com.example.nutri_capture_new.nutrient

import com.example.nutri_capture_new.db.DayMealView
import com.example.nutri_capture_new.db.Meal
import java.time.LocalDate

sealed class NutrientViewModelEvent {
    data class InsertMeal(val meal: Meal, val date: LocalDate) : NutrientViewModelEvent()
    data class DeleteMeal(val meal: Meal) : NutrientViewModelEvent()
    data class DeleteDayMeal(val dayMeal: DayMealView) : NutrientViewModelEvent()
}

State์— ์ดˆ๊นƒ๊ฐ’์„ ํ• ๋‹นํ•˜๊ธฐ ์œ„ํ•œ ์ด๋ฒคํŠธ(InitializeState) ๊ทธ๋ฆฌ๊ณ  ๋ฌดํ•œ ์Šคํฌ๋กค ์ด๋ฒคํŠธ(LoadMoreItemsAfterLastDayMeal)๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.

 

#2-5 NutrientScreen.kt

...

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

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

๋ฌดํ•œ ์Šคํฌ๋กค ๊ด€๋ จ LaunchedEffect() ๋“ฑ์„ ์ œ๊ฑฐํ•œ ๋ชจ์Šต์ด๋‹ค.

 

#3 ์š”์•ฝ

ViewModel์ด ๋ณด์œ ํ•˜๋Š” Stateํ˜• ํ”„๋กœํผํ‹ฐ๋ฅผ StateFlowํ˜•์œผ๋กœ ๋ณ€๊ฒฝํ•จ์œผ๋กœ์จ, ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์‚ฌํ•ญ(CRUD)์ด DB๋กœ๋ถ€ํ„ฐ ViewModel์„ ๊ฑฐ์ณ View๊นŒ์ง€ ์•”์‹œ์ ์œผ๋กœ ์ „๋‹ฌ๋˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.

 

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

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

'+' ๋ฒ„ํŠผ์€ '์ „์†ก' ๋ฒ„ํŠผ๊ณผ ์—ญํ• ์ด ๊ฒน์น˜์ง€๋งŒ ์ผ๋ถ€๋Ÿฌ ์‚ญ์ œํ•˜์ง€ ์•Š์•˜๋‹ค. ์™œ๋ƒํ•˜๋ฉด ๋‹ค์Œ ๊ฒŒ์‹œ๊ธ€๋ถ€ํ„ฐ, ChatBar๊ฐ€ ๋” ์ƒ์„ธํ•œ ์˜์–‘ ์ •๋ณด๋ฅผ ๋‹ด์•„ INSERT๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ INSERT๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ '+' ๋ฒ„ํŠผ์€ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ข€ ๋” ์•ˆ์ •ํ™”๋˜๋ฉด ๊ทธ ๋•Œ ์‚ญ์ œํ•œ๋‹ค.

 

#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