오늘은 회원가입을 개발하는 중이었는데 기존에 설계된 ViewModel 및 화면 전환에 있어서 문제점을 발견하였다.
회원가입에 필요한 화면이 ScreenA, ScreenB, ScreenC가 있다고 가정하자.
이메일
및 비밀번호
입력이름
및 전화번호
입력화면 플로우는 다음과 같다.
ScreenA -> ScreenB(회원가입 요청) -> ScreenC
class SignUpViewModel @Inject constructor(
private val repository: SignUpRepository
): ViewModel(){
}
@Composable
fun ScreenA(
// '''
viewModel: SignUpViewModel = hiltViewModel(),
// '''
)
@Composable
fun ScreenB(
// '''
viewModel: SignUpViewModel = hiltViewModel(),
// '''
)
val navController = rememberNavController()
composable("ScreenA"){
ScreenA(
// '''
onNextButtonClicked = {
navController.navigate("ScreenB")
},
// '''
)
}
composable("ScreenB"){
ScreenB(
// '''
onNextButtonClicked = {
navController.navigate("ScreenC")
},
// '''
)
}
이러한 방식으로 그동안 많이 썼었는데 이번에 ScreenA와 ScreenB가 데이터를 공유해야 하는 과정에서 한계를 직면했다. 이 방식에는 문제가 존재한다.
위의 방식의 경우
ScreenA에서 ScreenB로 이동할 때 Hilt를 사용하더라도 ViewModel 인스턴스가 새로 생성된다.
- 각 화면은 자신의 고유한 NavBackStackEntry를 가진다.
- navController.navigatie()로 새로운 화면을 추가하면, 새로운 NavBackStackEntry가 생성된다. 이때 해당 엔트리에 맞는 새로운 ViewModel이 생성된다.
- hiltViewModel()은 현재 활성화된 NavBackStackEntry와 연결된 ViewModel을 제공한다.
위와 같은 이유 때문에 ScreenA에서 저장했던 데이터가 ScreenB에서 ViewModel을 통해 조회할 경우 초기값으로 나오는 문제가 발생하였다.
위와 같은 사유로 서버에 데이터를 전송할 때 BAD REQUEST인 HTTP 400 에러가 발생하였다.
Jetpack Compose에서는 navBackStackEntry를 활용해 네비게이션 스택이 ViewModel의 상태를 인지하도록 설정하여 불필요한 재생성을 방지할 수 있다.
@Composable
fun ScreenA(
// '''
viewModel: SignUpViewModel,
// '''
) {
// '''
}
@Composable
fun ScreenB(
// '''
viewModel: SignUpViewModel,
// '''
) {
// '''
}
@HiltViewModel
class SignUpViewModel @Inject constructor(
// '''
): ViewModel() {
// '''
}
composable("ScreenA"){ backStackEntry: NavBackStackEntry ->
val viewModel: SignUpViewModel = hiltViewModel(viewModelStoreOwner = backStackEntry)
ScreenA(
// '''
viewModel = viewModel,
onNextButtonClicked = {
navController.navigate("ScreenB")
},
// '''
)
}
val viewModel: SignUpViewModel = hiltViewModel(viewModelStoreOwner = backStackEntry)는 NavBackStackEntry를 기반으로 SignUpViewModel의 인스턴스를 새롭게 생성하거나 기존 인스턴스를 반환한다
composable("ScreenB"){ backStackEntry: NavBackStackEntry ->
val viewModel: SignUpViewModel = if (navController.previousBackStackEntry != null){
// 이전 화면이 존재하는 경우
hiltViewModel(viewModelStoreOwner = navController.previousBackStackEntry!!)
} else {
// 이전 화면이 존재하지 않는 경우
hiltViewModel()
}
ScreenB(
// '''
viewModel = viewModel,
onNextButtonClicked = {
navController.navigate("ScreenC")
},
// '''
)
}
이제 정상적으로 동작하는지 다시 로그를 찍어 확인해보겠다.
드디어 두 화면간에 데이터가 공유되어 정상적으로 API 호출이 이루어진 것이 확인된다.
hiltViewModel(backStackEntry: NavBackStackEntry) : 특정 NavBackStackEntry를 기반으로 ViewModel을 생성하거나 반환한다
공유 ViewModel의 장점: