App 개발 일지/Nutri Capture

Nutri Capture 백엔드 - ChatBar에 ViewModel 연결

interfacer_han 2025. 1. 2. 15:45

#1 개요

#1-1 ViewModel 전달하기

ChatBar의 '전송' 버튼을 눌러, NutrientViewModel.onEvent()를 발생시킬 것이다. 현재 MainActivity에서 NutrientViewModel 객체를 만들어서 NutrientScreen에 전달하고 있는데, 같은 방식으로 ChatBar에도 전달한다. 그리고 이 NutrientViewModel에 정의해둔 Insert 동작이, ChatBar의 '전송' 버튼을 누를 때 트리거되게 만든다.

 

지금은 기존 코드를 복사해서 새로운 기능에 붙여넣기하고 있지만, 후에 Hilt로 마이그레이션해서 이러한 의존성 관련 코드들을 깔끔하게 정리할 예정이다.

 

#1-2 문제점: UI 업데이트

하지만 #1-1를 프로젝트에 적용해도, #3-1에서 보듯 UI가 업데이트 되지 않는다. '+' 버튼을 누르는 경우에는 잘 되는 것과 대조적으로 말이다.

 

...

class NutrientViewModel(private val repository: MainRepository) : ViewModel() {
    // (1) 화면 표시용 State
    private val _nutrientScreenState = mutableStateOf(
        NutrientScreenState(
            dayMeals = SnapshotStateList()
        )
    )
    val nutrientScreenState: State<NutrientScreenState>
    get() = _nutrientScreenState
    
    ...
}

...

문제의 원인은 NutrientViewModel이 보유하는 State의 구조에 있다. 현재의 구조는 '+ 버튼 눌림 이벤트'를 감지한 경우에만 _nutrientScreenState에 원소를 하나하나 할당하는 방식이다. 만약, 데이터베이스가 ViewModel의 '+ 버튼 눌림 이벤트' 외 다른 방식으로 변경된다면 _nutrientScreenState는 변하지 않을 것이다.

 

State를 StateFlow로 변경하고, 그 StateFlow가 데이터베이스를 구독하게 만들어야 한다. 그러면 데이터베이스가 변경될 때마다 StateFlow가 암시적으로 변경될 것이다. 이렇게 바꾸면 현재의 방식보다 훨씬 편하고, 훨씬 깔끔하고, 훨씬 에러의 여지가 적다.

 

다음 게시글에서 이 변경 사항을 적용한다.

 

#2 코드

#2-1 MainActivity.kt

...

class MainActivity : ComponentActivity() {

    private lateinit var dao: MainDAO
    private lateinit var repository: MainRepository

    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        dao = MainDatabase.getInstance(application).mainDAO
        repository = MainRepository(dao)

        enableEdgeToEdge()
        setContent {
            NutricapturenewTheme {
                ...

                Scaffold(
                    ...
                    bottomBar = {
                        when (currentRoute) {
                            Destination.NutrientScreen.route -> NutrientChatBar(
                                viewModel(
                                    factory = NutrientViewModelFactory(repository)
                                )
                            )

                            else -> MainNavigationBar(navController)
                        }
                    },
                    ...
                ) { ...
                    NavHost(
                        ...
                    ) {
                        composable(route = Destination.NutrientScreen.route) {
                            NutrientScreen(
                                scope = scope,
                                snackbarHostState = snackbarHostState,
                                viewModel = viewModel(factory = NutrientViewModelFactory(repository))
                            )
                        }
                        ...
                    }
                }
            }
        }
    }

    @Composable
    private fun MainNavigationBar(navController: NavHostController) {
        ...
    }

    @Composable
    fun NutrientChatBar(viewModel: NutrientViewModel) {
        Row(
            ...
        ) {
            ...

            Box(
                ...
            ) {
                FilledTonalIconButton(
                    onClick = {
                        viewModel.onEvent(
                            NutrientViewModelEvent.InsertMeal(
                                meal = Meal(
                                    time = LocalTime.now(),
                                    name = inputtedText.value,
                                    nutritionInfo = NutritionInfo()
                                ),
                                date = LocalDate.now()
                            )
                        )
                        inputtedText.value = ""
                    },
                    ...
                ) {
                    ...
                }
            }
        }
    }
}

...

composable(route = Destination.NutrientScreen.route) { ... } 속에 있던, dao 및 repository 초기화 코드를 밖으로 빼서 MainActivity 내에서 사용할 수 있게 한다. 초기화는 onCreate()에서 진행한다.

#2-2 NutrientScreen.kt

...

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

    LazyColumn(
        ...
    ) {
        ...
        items(...) { ... ->
            ...

            Card(
                ...
            ) {
                Box(
                    ...
                ) {
                    Column(
                        ...
                    ) {
                        Text(
                            ...
                        )

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

                    ...
                }
            }
        }
    }
}

 

TextField에 입력된 name을 확인할 수 있도록 수정한다.

 

#3 완성된 앱

#3-1 스크린샷

#1-2 참조

 

#3-2 이 게시글 시점의 Commit

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com

 

#3-3 본 프로젝트의 가장 최신 Commit

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com