App 개발 일지/Nutri Capture

Nutri Capture 백엔드 - Model을 ViewModel에 생성자 주입

interfacer_han 2024. 10. 23. 14:36

#1 개요

이전 게시글에서 구축한 Room을 ViewModel에 생성자 주입하여, Room(Model)을 참조할 수 있게 만든다.

 

#2 코드 - 이벤트 추가

#2-1 이벤트 추가

// package com.example.nutri_capture_new.nutrient

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

sealed class NutrientViewModelEvent {
    data object InitializeState : NutrientViewModelEvent()
    data object LoadMoreItemsAfterLastDate : NutrientViewModelEvent()
    data object LoadMoreItemsBeforeFirstDate : NutrientViewModelEvent()
    data class InsertMeal(val meal: Meal, val date: LocalDate) : NutrientViewModelEvent()
    data class DeleteMeal(val meal: Meal, val date: LocalDate) : NutrientViewModelEvent()
    data class GetMealsByDate(val date: LocalDate) : NutrientViewModelEvent()
}

class의 이름은 MainRepository의 함수 이름에서 따왔다. 새로 추가한 이 3개의 클래스가 ViewModelEvent의 자식 클래스인 것에서 알 수 있듯, InsertMealㆍDeleteMealㆍGetMealsByDate는 View에서 트리거되어 ViewModel에서 구현될 이벤트들이다.

 

#2-2 ViewModel 변경

...

class NutrientViewModel : ViewModel() {
    ...

    // (4) View로부터 받은 이벤트 처리
    fun onEvent(event: NutrientViewModelEvent) {
        when (event) {
            ...
            
            is NutrientViewModelEvent.InsertMeal -> {
                // TODO
            }
            
            is NutrientViewModelEvent.DeleteMeal -> {
                // TODO
            }
            
            is NutrientViewModelEvent.GetMealsByDate -> {
                // TODO
            }
        }
    }
}

ViewModel의 onEvent() 함수 속 When(NutrientViewModelEvent)의 branch들을 추가해준다.

 

#3 ViewModel 팩토리 추가

#3-1 ViewModel 팩토리 추가

// package com.example.nutri_capture_new.nutrient

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.nutri_capture_new.db.MainRepository

class NutrientViewModelFactory(private val repository: MainRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(NutrientViewModel::class.java)) {
            return NutrientViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown View Model Class")
    }
}

이전 게시글에서 만든 Repository를 ViewModel이 참조하는 관계이기에, Repository를 ViewModel의 생성자 인수 자리에 넣어줄 예정이다. ViewModel에 생성자가 존재하는 경우 ViewModel의 팩토리를 만들어주어야 하므로 위와 같은 팩토리 클래스를 만든다. (참조: [Android] ViewModel - 뷰 모델에 인자(Argument) 전달 (ViewModelFactory))

 

#3-2 ViewModel에 생성자 인수 추가

...

class NutrientViewModel(private val repository: MainRepository) : ViewModel() {
    ...
}

ViewModel에 생성자 인수를 넣어준다.

 

#3-3 NutrientScreen 생성자 중 ViewModel 인수의 기본값 제거

...

@Composable
fun NutrientScreen(
    ...
    viewModel: NutrientViewModel,
    ...
) {
    ...
}

NutrientScreen의 생성자 인수에 있던, viewModel: NutrientViewModel = viewModel<NutrientViewModel>() 구문을 삭제하고 대신 viewModel: NutrientViewModel를 넣어준다. 왜냐하면 ViewModel은 이제 생성자 인수가 존재하기 때문이다. NutrientScreen의 상위 모듈 즉, MainActivity에서 'Repository가 주입된 ViewModel'을 NutrientScreen에 전달하는 식으로 구조를 바꿀 것이다.

 

#3-4 MainActivity에서 ViewModel 팩토리를 이용해 NutrientScreen 제작

...

class MainActivity : ComponentActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setContent {
            NutricapturenewTheme {
                ...

                Scaffold(
                    ...
                ) { ... ->
                    NavHost(
                        ...
                    ) {
                        composable(route = Destination.NutrientScreen.route) {
                            val dao = MainDatabase.getInstance(application).mainDAO
                            val repository = MainRepository(dao)
                            NutrientScreen(
                                scope = scope,
                                snackbarHostState = snackbarHostState,
                                viewModel = viewModel(factory = NutrientViewModelFactory(repository))
                            )
                        }
                        composable(route = Destination.StatisticsScreen.route) {
                            ...
                        }
                        composable(route = Destination.UserInfoScreen.route) {
                            ...
                        }
                    }
                }
            }
        }
    }

    ...
}

...

참조: [Android] Jetpack Compose - ViewModel에서 State 사용하기

 

#4 View에 표시할 State 구조 변경

#4-1 ScreenState 변경

// package com.example.nutri_capture_new.nutrient

import androidx.compose.runtime.snapshots.SnapshotStateList
import com.example.nutri_capture_new.db.Meal
import java.time.LocalDate

data class NutrientScreenState(
    val listOfDateAndMeals: SnapshotStateList<DateAndMeals>
)

data class DateAndMeals(
    val date: LocalDate,
    val meals: SnapshotStateList<Meal>
)

변경된 ERD 및 Entity 구조에 맞게, ScreenState 구조도 변경한다.

 

#4-2 ViewModel 코드 업데이트

...

class NutrientViewModel(private val repository: MainRepository) : ViewModel() {
    // (1) 화면 표시용 State
    private val _nutrientScreenState = mutableStateOf(
        NutrientScreenState(
            listOfDateAndMeals = SnapshotStateList()
        )
    )
    ...

    // (2) ViewModel용 내부 변수
    ...

    // (3) View에서 받아 처리할 이벤트
    ...

    // (4) View로부터 받은 이벤트 처리
    fun onEvent(event: NutrientViewModelEvent) {
        when (event) {
            is NutrientViewModelEvent.InitializeState -> {
                ...
                repeat(20) {
                    _nutrientScreenState.value.listOfDateAndMeals.add(
                        DateAndMeals(
                            ...
                        )
                    )
                    ...
                }

                ...
            }

            is NutrientViewModelEvent.LoadMoreItemsAfterLastDate -> {
                val lastDate = _nutrientScreenState.value.listOfDateAndMeals.last().date
                _nutrientScreenState.value.listOfDateAndMeals.add(
                    DateAndMeals(
                        ...
                    )
                )
            }

            is NutrientViewModelEvent.LoadMoreItemsBeforeFirstDate -> {
                val firstDate = _nutrientScreenState.value.listOfDateAndMeals.first().date
                _nutrientScreenState.value.listOfDateAndMeals.add(
                    ...,
                    DateAndMeals(
                        ...
                    )
                )
            }

            is NutrientViewModelEvent.InsertMeal -> {
                // TODO
            }

            is NutrientViewModelEvent.DeleteMeal -> {
                // TODO
            }

            is NutrientViewModelEvent.GetMealsByDate -> {
                // TODO
            }
        }
    }
}

#4-1의 변경사항에 맞춰 ViewModel의 코드도 업데이트한다.

 

#4-3 View 코드 업데이트

...

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

    LazyColumn(
        ...
    ) {
        val listOfDateAndMeals = viewModel.nutrientScreenState.value.listOfDateAndMeals
        items(listOfDateAndMeals) { dateAndMeals ->
            Card(
                ...
            ) {
                Column(
                    ...
                ) {
                    Text(
                        text = DateFormatter.formatDateForNutrientScreen(dateAndMeals.date),
                        ...
                    )

                    ...
                }
            }
        }
    }
}

#4-1의 변경사항에 맞춰 View의 코드도 업데이트한다.

 

#5 요약

구축한 Model 데이터를 ViewModel에서 참조할 수 있게 만들었다. 또, View에서 Model 데이터 관련한 이벤트를 ViewModel에 요청할 수 있는 구조를 잡았다.

 

#6 완성된 앱

#6-1 이 게시글 시점의 Commit

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com

 

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

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com