MVVM 디자인 패턴의 예제 프로젝트를 만들던 도중에 다음과 버그가 일어났다.
화면을 먼저 살펴보자.
메인화면과 MVVM 버튼을 눌렀을 때 화면전환이 된 모습이다.
실제로 Room DB / Retrofit2 & OkHttp 와 같은 DB를 접근해서 가져오는 것은 아니고,
특정 클래스에 미리 담아놓은 값들을 가져오는 것이다.
해당 화면에서 AAC ViewModel, LiveData를 사용해서 수정 완료 버튼을 누르면
AAC ViewModel이 Repository를 통해서 값을 수정하게 되는 로직이다.
코드는 조만간 정리될 디자인 패턴 정리 글에 의해 올려질 예정이지만 잠깐 살펴보자면,
모델 클래스
/**
* Design Pattern에서 Model이란, Data를 포함해서 Data를 송, 수신하는 모든 행위를 말한다.
* 여기서 UserModel 클래스는 Room DB (SQLite) / Retrofit2 등
* Local / Server DB 통신을 하는 매개체이다.
*/
class UserModel {
private val userInfo = User("NO_EMAIL", "NO_NAME", "NO_CONTACT", "NO_ADDRESS", "-1")
fun getUserInfo(): User {
return userInfo
}
fun modifyUserInfo(_userEmail: String, _userName: String, _userContact: String, _userAddress: String, _userAge: String) {
userInfo.apply {
userEmail = _userEmail
userName = _userName
userContact = _userContact
userAddress = _userAddress
userAge = _userAge
}
}
}
디자인 패턴에서의 Model 클래스이다.
그냥 말 그대로 Data를 처리하는 부분이라고 생각하면 된다.
Fragment에서 ViewModel을 초기화하고 Observer을 사용하는 코드
AAC ViewModel을 MVVM ViewModel처럼 사용하기 위해, AAC ViewModel을 Data Binding 해서 사용했다.
ViewModel을 초기화하는 메서드와, Observer를 셋팅하는 메서드가 있다.
이 두개는 Fragment에서 onCreateView() 위치에서 호출되게 된다.
얼핏 보기엔 문제가 없어보인다.
뷰모델의 Owner를 Activity로 설정했고, Observer도 잘 설정해주는 것 같다.
다시 화면으로 돌아와서 살펴보자.
해당 화면에서 유저 정보 수정 입력 -> 유저 정보 수정 요청 -> 수정된 정보 불러오기 의 로직으로
정상적으로 수행이 되는 것 처럼 보인다.
수정 요청을 한 뒤에 받아온 값이 true일 때 로딩 중 창이 나오고 수정된 정보를 받아와야 한다.
따라서 저 로딩 중 창은 수정을 요청했을 때만 나타나야 한다.
하지만 메인화면으로 나갔다가 다시 들어오게 될 경우에도 수정 완료라고 받아오게 된다.
무엇이 문제일까?
우선, ViewModel의 Owner를 Activity로 설정했다.
이것은 혹시 모를 다른 Fragment와 ViewModel을 같이 사용할 수도 있기 때문에 이렇게 만든 것이다.
그러고나서 Observer를 Setting했다.
Observer의 Owner는 viewLifeCycleOwner이다.
따라서 내가 생각한 로직 대로라면,
프래그먼트가 종료되면 Observer는 삭제되고
프래그먼트가 재생성되면 Observer는 다시 생성되는 것이다.
이 로직은 맞긴 맞았다. 다만, Observer에 대해 내가 모르는 점이 있었다.
- Observer는 Set한 순간에도 LiveData를 구독하기 때문에,
구독하는 LiveData의 값이 null이 아니라면 Observe Logic을 수행하게 된다.
ViewModel에는 LiveData의 값이 남아있다.
왜냐하면 Owner가 Activity이기 때문이다. Fragment가 종료됐다고 해서 ViewModel은 사라지는 것이 아니다.
따라서 Observer는 "어? 구독하는 LiveData의 값이 Null이 아니네?"라고 생각해서
제 할 일을 한 것이었다.
어떻게 수정하면 좋을까 찾아보았다.
두 가지 방법을 찾았다.
1. Fragment에서만 ViewModel 사용 / Fragment가 종료되면 ViewModel도 사라지게:
Fragment에서 ViewModel의 Owner 수정
ViewModelProvider의 생성자로 세 가지가 있다.
보면 알겠지만 결국 ViewModelStore를 쓴다는 것이다.
ViewModelStoreOwner에는 ViewModelStore를 가지고 있다.
ViewModelStore는 HashMap의 형태이다.
HashMap의 형태로 ViewModel들을 관리하는 것이다.
Activity / Fragment에서는 ViewModelStoreOwner 인터페이스를 구현하고 있기 때문에
Activity / Fragment에서 this를 써도 구현이 되는 것이다.
즉, 액티비티나 프래그먼트에서 this를 쓰든 viewModelStore를 쓰든 같은 의미라는 뜻이다.
위처럼 ViewModel의 owner를 this 혹은 viewModelStore로 지정할 수 있다.
- viewModelStore : 액티비티 / 프래그먼트의 ViewModelStore이다.
- this : 액티비티 / 프래그먼트 그 자체이다.
ViewModelStoreOwner 인터페이스를 구현하기 때문에 가능하다.
이렇게하면, Activity / Fragment가 onDestroy되면 해당 뷰모델이 초기화가 된다.
따라서 다시 Fragment에 접근해도 문제가 생기지 않게된다.
(물론 ViewModel은 Activity / Fragment의 생명주기와는 별개라고 한다.
그러나 UI 컨텍스트만 ViewModel에서 다루지 않는다면 큰 문제는 생기지 않는다.)
그런데 만일,
Acitivty에 생성하고 다른 Fragment들에서도 ViewModel을 공유해서 사용하고 싶을 땐 어떻게 할까?
2. 다른 Fragment에서도 ViewModel 공유 :
Event Wrapper 사용.
Event Wrapper라는 것이 존재한다.
SingleLiveEvent라는 것도 있지만, Observer를 하나밖에 Setting하지 못한다고 한다.
하지만 우리가 하고 싶은 것은 여러 프래그먼트에서 ViewModel의 LiveData를 Observe하고싶은 것이다.
따라서 SingleLiveEvent를 사용하는 것은 Pass한다.
위 클래스를 구현해준다. 사용은 다음과 같다.
- 예를들어, User 클래스가 있다면 User는 Event(User) / <User>는 <Event<User>>로 사용한다.
- Event Wrapper로 만들어진 클래스에서는 다음과 같은 메서드들이 Return한 값으로 Observe한다.
1. contentIfNotHandled() : 한 번 Observe 한 값은 null을 Return한다. 한 번도 Observe하지 않은 값은 해당 값을 Return해준다. (당연하게도, 제일 최신의 Observe하지 않은 값을 Return한다. 여러 개의 Observe하지 않은 값이 있어도 가장 최신의 값만.)
2. peekContent() : Observe를 했건 안했건 제일 최신의 값을 Return한다.
EventWrapper를 쓰고 안쓰고의 차이는 위 LiveData와 아래 LiveData의 구현이다.
postValue할때도 Event({값})이 되면 된다.
그러면 이제 Activity가 Owner인 LiveData를 한 번만 Observe하는 방법은?
이렇게 바꿔주면 된다.
필요에 따라서 최신의 값을 쓰든지, 한 번 Observe한 값은 안 쓸지 결정할 수 있다는 점이 가장 큰 메리트인 것 같다.
MVVM은 공부하면 할수록 어려운 디자인 패턴인 것 같다...
LiveData를 잘 활용하고 싶은데 아직도 어렵다.