그렇다면 이제 MVVM 패턴을 적용할때.. A와 B화면은 세부적으로 가지고 있는 view들이 전부 다 다른데
A와 B 두화면의 공통된 viewmodel에는 어떤 데이터?들이 들어가야하나요?
위의 질문의 답은 님이 구하셔야 하는 답입니다. 왜냐하면 뷰의 구성과와 비지니스 요구사항에 ViewModel에서 다루어야 하는 데이터도 달라지기 때문입니다. 따라서 구체적으로 어떤 데이터를 가져야 하는지 딱히 일반화 시켜서 말씀드리기는 어렵습니다. 한가지 명확한 것은 ViewModel은 Model(보통 API 또는 DB) 등에서 오는 데이터를 뷰에서 보여주기 좋은 형태로 변환해서 전달하는 역할을 한다는 것입니다. 따라서 ViewModel을 공유하는 것은 사실 다른 하면 사이의 데이터를 어떻게 잘 처리할지 하는 고민이 생기기 때문에 어느정도의 trade off가 존재하게 되는 겁니다.
만약 두개의 화면이 다 복잡하다면 처음으로 돌아가서 과연 꼭 ViewModel 을 공유해야만 하는지 디자인에 대한 리뷰를 해보시고, 꼭 필요하다면 ViewModel에는 두 화면이 공통적으로 가지는 데이터만 보관하고 화면 별로 별도의 클래스를 만들어서 이 클래스들에 실제 뷰에 대한 기능을 위임하는 것도 옵션이 될 수 있습니다.
기본적인 아이디어는 아래와 같습니다.
sealed class AuthCodeRequestResult {
data class Error(val e: Exception): AuthCodeRequestResult()
data class Success(val userName: String, message: String): AuthCodeRequestResult()
}
//Note.ViewModel이 아님
class AuthCodeRequestSubViewMoel (
// Dependencies
) {
fun send(userName: String): AuthCodeRequestResult {...}
}
sealed class AuthCodeSubmitResult {
...
}
//Note.ViewModel이 아님
class AuthCodeSubmitSubViewMoel(
// Dependencies
) {
fun submit(userName: String, authCode: String): AuthCodeSubmitResult {...}
}
class AuthCodeViewModel(
private val authCodeRequstSub: AuthCodeRequestSubViewMoel,
private val authCodeSubmitSub: AuthCodeSubmitSubViewMoel,
): ViewModel() {
private val _authCodeRequestResultLiveData = MutableLiveData<AuthCodeRequestResult>()
val authCodeRequestResultLiveData: LiveData<AuthCodeRequestResult> get() = _authCodeRequestResultLiveData
private val _authCoeSubmitResultLiveData = MuableLiveData<AuthCodeSubmitResult>()
val authCoeSubmitResultLiveData: LiveData<AuthCodeSubmitResult> get() = _authCoeSubmitResultLiveData
fun sendAuthCodeClicked(userName: String) {
val result = authCodeRequstSub.send(userName)
_authCodeResultLiveData.postValue(result)
}
fun submitAuthCode(authCode: String) {
val userName = authCodeResult.value?.userName ?: throw IllegalStateException("Username cannot be null")
val result = authCodeSubmitSub.submit(userName = userName, autCode = authCode)
_authCoeSubmitResultLiveData.postValue(result)
}
}
class AuthCodeRequestFragment: Fragment() {
private val viewModel: AuthCodeViewModel by ...
override fun onViewCreated(...) {
super.onViewCreated(...)
viewModel.authCodeRequestResultLiveData.observe(viewLifeCycleOwner) { result: AuthCodeRequestResult ->
when (result) {
is AuthCodeRequestResult.Error -> showError()
is AuthCodeRequestResult.Success -> navigateToAuthCodeSubmitFragment()
}
}
}
fun sendButtonClicked() {
viewModel.sendAuthCodeClicked(userNameText.text.toString())
}
}
class AuthCodeSubmitFragment: Fragment() {
private val viewModel: AuthCodeViewModel by ...
override fun onViewCreated(...) {
super.onViewCreated(...)
viewModel.authCodeRequestResultLiveData.observe(viewLifeCycleOwner) {
when (result) {
is AuthCodeRequestResult.Error -> throw IllegalStateException("This screen must not be shown when authcode request failed.") // app crash
is AuthCodeRequestResult.Success -> {
messageText.text = result.message
}
}
}
viewModel.authCoeSubmitResultLiveData.observe(viewLifeCycleOwner) { result: AuthCoeSubmitResultL->
when (result) {
is AuthCoeSubmitResultL.Error -> showError()
is AuthCoeSubmitResultL.Success -> showSuccess()
}
}
}
fun submitClicked() {
viewModel.submitAuthCode(authCodeText.text.toString())
}
}
위의 예제는 실제 현업에서도 많이 사용되는 플로우 중의 하나인데요. 예를 들면, 비번을 잊어버리면 사용자 이름을 넣고 SMS를 인증코드를 요청한 다음, 요청 성공시 다음화면으로 이동해서 인증코드를 검증하는 예입니다. 물론 화면의 구성은 요구사항에 따라 다를 수 있지만 화면 간에 데이터 공유를 어떻게 처리하는지만 보시기 바랍니다. 두 화면 간에 authCodeRequestResultLiveData를 공유하고 있고 각각의 화면에 대한 처리는 해당 *SubViewModel이라는 별도의 클래스에 위임을 하고 있는 점에 주목하세요.
지극히 개인적인 선호사항이 될 수도 있는데, 저 같으면 ViewModel을 복잡하게 만드는 것 보다는 그냥 Navigation Component에 bundle을 넘겨서 처리할 것 같긴합니다. 왜냐면 ViewModel을 공유하게 되면 화면 간에 dependency(의존성?)가 생겨서 코드를 재사용하기가 더 어려워 질 뿐더러, 다른 화면에서 호출하지 말아야할 ViewModel의 함수를 호출할 가능성을 열어두게 되는 것이고 다른 두 화면의 코드를 하나의 클래스에 위치시키는 것은 그다지 좋은 디자인 같아 보이지는 않습니다. 물론 위처럼 간단한 예라면 문제가 없겠지만요.