✏️ ViewModel
1. ViewModel
- ViewModel 클래스는 생명 주기를 인식하며, UI와 관련된 데이터를 저장하고 관리한다.
- ViewModel 클래스는 화면 회전 같은 구성 변경에서도 살아남아 데이터를 보존한다.
- 사용자에의해 시스템이 UI 컨트롤러를 파괴하거나 재생성하면, 일시적으로 UI 관련 데이터를 모두 잃는다.
- 간단한 데이터는 onSaveInstanceState()로 저장하고 bundle로부터 데이터를 되살릴 수 있지만 큰 데이터에는 적합하지 않다.
- 또한 사용자의 행동에 반응하고, UI로 시스템과의 상호 작용을 해야하는 UI 컨트롤러가 데이터를 로드하는 것까지 맡으면 클래스가 비대해져 이를 분리하는 것이 효율적이다.
2. ViewModel 구현하기
- ViewModel 객체는 구성 변경에도 자동으로 유지되므로, ViewModel이 보유한 데이터는 다음 액티비티나 프래그먼트 인스턴스에서 즉시 사용할 수 있다.
- 아래 예제에서는 ViewModel이 UI 컨트롤러 대신 사용자 목록을 불러오고 데이터를 유지한다.
- 불러온 데이터는 LiveData로 관리하기를 권장한다.
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
}
}
- 액티비티에서는 아래와 같이 데이터에 접근할 수 있다.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
})
}
}
- 액티비티가 재생성되더라도 액티비티가 최초에 생성한 ViewModel 인스턴스를 그대로 가져오고, ViewModel을 생성한 액티비티가 종료되어야 ViewModel의 onCleared()가 호출되어 리소스를 정리한다.
- ViewModel은 View나 LifecycleOwner보다 오래 지속되도록 설계되었다.
- 또한 View나 Lifecycle에 의존성이 없다.
- ViewModel 내부에서 생명 주기 이벤트를 구현하도록 LifecycleObserver를 포함할 순 있지만, ViewModel 내부에서 LiveData의 변경 사항을 관찰해서는 안된다.
- ViewModel에서 Application Context가 필요하면 AndroidViewModel 클래스를 사용할 수 있다.
3. ViewModel의 생명 주기
- 생명 주기를 인식하는 ViewModel 객체를 얻으려면 Lifecycle을 ViewModelProvider에 넘겨야 한다.
- ViewModel은 Lifecycle이 영구적으로 없어질 때까지(activity에서는 activity가 끝날 때까지, fragment에서는 fragment가 분리될 때까지) 메모리에 남아 있다.
- 아래 그림은 액티비티가 회전하고 종료하기까지 생명 주기와 액티비티와 연관된 ViewModel의 생명주기다.
- 일반적으로 ViewModel 객체의 생성은 액티비티의 첫 onCreate() 호출에서 이뤄지고, 구성 변경에 의해 여러번 액티비티가 재생성되어 onCreate()가 또 호출되어도 최초에 생성된 ViewModel 인스턴스를 유지한다.
- ViewModel은 액티비티가 종료되고 파괴된 후에 onCleared()가 호출되어 리소스를 정리한다.
4. 프래그먼트 간 데이터 공유
- Activity Scope로 생성한 ViewModel은 여러 프래그먼트 사이에서 데이터를 공유하고 상호 작용할 수 있도록 한다.
- 아래 예제는 MasterFragment와 DetailFragment가 SharedViewModel을 이용해 데이터를 공유하는 것이다.
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
model.select(item)
}
}
}
class DetailFragment : Fragment() {
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
})
}
}
- 프래그먼트에서 뷰 모델을 생성하는데 사용한 액티비티를 얻고, 이를 사용해 ViewModelProviders로부터 공통된 SharedViewModel 인스턴스를 얻을 수 있다.
- 이 방식에는 아래와 같은 이점이 있다.
- 전통적인 방법은 이벤트를 전달하는 interface를 구현해 액티비티가 중간 연결 역할을 했지만, ViewModel을 사용하면 액티비티는 아무것도 안해도 된다.
- 프래그먼트는 SharedViewModel을 사용할 때 선행되는 작업이 없다. 프래그먼트 중 하나가 사라져도 잘 작동된다.
- 각 프래그먼트는 자체 수명 주기가 있으며, 다른 프래그먼트에 영향을 주지 않는다. 한 프래그먼트가 다른 프래그먼트를 대체해도, UI는 아무 문제 없이 계속 작동한다.
5. ViewModel 사용 시 주의점
(1) ViewModel은 절대 액티비티 같은 Context를 참조하면 안된다
- 액티비티는 언제든 파괴되고 재생성될 수 있지만, ViewModel은 유지된다.
- 이때 이전에 파괴된 액티비티를 ViewModel에 유지하면 메모리 누수가 발생한다.
- 그러므로 Context가 필요하다면 AndroidViewModel을 사용한다.
(2) ViewModel에서 안드로이드 프레임워크 코드를 참조하면 안된다
- 프레임워크 코드를 참조하면 단위 테스트가 힘들어진다.
- ViewModel은 View에 표현할 최소한의 데이터만 갖고, 일반적인 비즈니스 로직은 다른 계층에서 수행한다.
(3) Dagger2와 ViewModel의 사용을 신중히 한다
- Dagger2는 자신만의 Scope를 지정하고 인스턴스를 관리하며, ViewModel 또한 스스로 인스턴스를 관리한다.
- 둘의 Scope는 다르므로 Dagger2의 오브젝트 그래프에서 제공하는 인스턴스와 ViewModel이 같이 사용되는 경우 액티비티가 재생성됐을 때 객체의 동일성이 깨지기 쉽다.
(4) 리소스 아이디를 참조해 올바른 언어의 문자열을 참조한다
- ViewModel에서 getString(int)을 통해 문자열을 관리하는 경우, 시스템 언어를 변경해도 이전 언어의 문자열이 그대로 남는다.
- ViewModel과 View가 바인딩될 때, 리소스 아이디를 참조해 올바른 언어의 문자열을 참조한다.