ViewModel
안드로이드는 화면 회전과 같이 구성을 변경할 때
UI 컨트롤러를 제거하거나 다시 만들어지면서 UI 컨트롤러에 저장된 모든 일시적인 UI 관련 데이터가 삭제됩니다.
cf) 그래서 기존에는 onSaveInstanceState() 메서드를 사용하여 데이터 삭제 전 보관하였으나 제한적이었습니다.
onSaveInstanceState() 메서드를 사용하여 onCreate()의 번들에서 데이터를 복원할 수 있습니다.
하지만 이 접근 방법은
1) 사용자 목록이나 비트맵과 같은 대용량일 가능성이 높은 데이터가 아니라, 직렬화했다가 다시 역직렬화할 수 있는 소량의 데이터에만 적합합니다.
2) 다른 문제는 UI 컨트롤러가 반환하는 데 시간이 걸릴 수 있는 비동기 호출을 자주 해야 한다는 점입니다. UI 컨트롤러는 비동기 호출을 관리해야 하며, 메모리 누수 가능성을 방지하기 위해 호출이 제거된 후 시스템에서 호출을 정리하는지 확인해야 합니다. 관리에는 많은 유지관리가 필요하며, 구성 변경 시 개체가 다시 생성되는 경우 개체가 이미 수행된 호출을 다시 호출해야 할 수 있으므로 리소스가 낭비됩니다.
(그래서 구글은 UI 컨트롤러 로직에서 뷰 데이터 소유권을 분리하는 방법이 훨씬 쉽고 효율적이라 생각하여)
따라서, 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 ViewModel 클래스가 탄생하였습니다.
이로 인해 이제
- 생명 주기에 영향을 받지 않고 데이터를 유지할 수 있다.
- UI 컨트롤러와 데이터가 분리된다. (Clean Arichtecture)
- 프래그먼트 간의 데이터 공유가 쉬워진다.
ViewModel의 생명주기
※ 주의: ViewModel은 View, Lifecycle 또는 context 참조를 포함하는 클래스를 참조해서는 안 됩니다!!
ㄴ> 이유
뷰 모델은 액티비티나 프래그먼트보다 긴 생명주기를 가지고 있다.
액티비티는 종료와 생성을 반복하겠지만 뷰 모델은 쭉 살아있기 때문에 이미 종료되어 사라진 액티비티의 참조를 그만큼 가지고 있을 것이다.
ex) 화면 100번 회전 시 ViewModel이 이미 사라진 액티비티 참조 다 가지고 있다;;
쓸데없는 것이 메모리를 차지하고 있는 현상, 즉 Memory Leak이 발생하기 때문에 참조를 하면 안 된다는 것이다.
ㄴ> LiveData 같은 LifecycleObservers 포함은 가능!
그러나 LiveData처럼 수명 주기를 인식하는 Observable의 변경사항을 관찰하는 것은 절대안됨!!
ㄴ> AndroidViewModel을 상속받아 applicationContext는 참조를 해도 괜찮다.
(∵ 액티비티의 생명주기가 아닌 애플리케이션의 생명주기를 가지기 때문에)
ViewModel은 액티비티 혹은 프래그먼트와 다른 생명주기를 가지게 된다.
ㄴ> ViewModel 생명 주기가 더 길다
ㄴ> 소유자 활동이 완료되면 (finish 메서드가 호출됐을 때 혹은 사용자가 직접 뒤로 가기 버튼을 눌러 액티비티를 종료했을 때 등)
프레임워크는 리소스를 정리할 수 있도록 onCleared() 메서드를 호출하여 ViewModel 소멸된다.
0. ViewModel 설정
dependencies {
def lifecycle_version = "2.3.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}
ㄴ> LiveData는 일반적으로 ViewModel 내에 존재하여 같이 기재함.
1. ViewModel 클래스 생성
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() {
// Do an asynchronous operation to fetch users.
}
}
ㄴ> LiveData는 일반적으로 ViewModel 내에 존재한다.
2. ViewModel 객체 생성
ㄴ> 일반적으로 액티비티 onCreate()메서드를 처음 호출할 때 ViewModel을 요청
시스템은 활동 기간 내내(예: 기기 화면이 회전될 때) onCreate() 메서드를 여러 번 호출할 수 있습니다. ViewModel이 처음 요청되었을 때부터 활동이 끝나고 폐기될 때까지 ViewModel은 존재합니다.
ㄴ> ViewModel을 각각 다른 소유자가 생성하면 이는 별개의 메모리 공간을 사용하는 다른 객체가 된다.
ㄴ> 하나의 액티비티를 소유자로 지정해 사용하면 같은 ViewModel을 공유할 수 있다. = 데이터 공유 가능 -> Fragment간 데이터 공유
2-1. ViewModelProvider 로 생성하는 법
class MainActivity : AppCompatActivity() {
private lateinit var userViewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ViewModel 생성
userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
// ViewModel 내 LiveData 관찰
userViewModel.user.observe(this, Observer {
// UI업데이트
binding.textViewHeight.text = it.toString()
})
}
}
ViewModelProvider(this).get(UserViewModel::class.java)
ㄴ> 'this'는 owner를 의미 (owner가 'ViewModelStore(ViewModel 객체가 HashMap 구조로 저장되는 곳)'을 소유한다.)
ㄴ> 그러므로 get() 에 전달되는 'UserViewModel::class.java'는 ViewModel객체를 찾아오기 위한 Key값으로 쓰인다.
+) 만약 Key에 해당하는 Value가 없으면 생성하고 가져온다.
그래서 처음 뷰 모델 객체를 처음 만드는데도 set 따위가 아니라 get을 쓰는 것이며
이 ViewModelStore를 소유하고 있는 주체가 MainActivity라는 것을 알려주는 것이다.
2-2. activity-ktx 'by viewModels()' Kotlin 위임 프로퍼티로 생성
ㄴ> 데이터를 공유할 것이 아니라면 ⓑ 방법이 더 간결하여 편리
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ViewModel 생성 : activity-ktx에서 'by viewModels()' Kotlin 위임 프로퍼티를 사용하여 생성함
val model: MyViewModel by viewModels()
// ViewModel 내 LiveData 관찰
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
ViewModel의 여러 사용처
1) 프래그먼트 간 데이터 공유
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class ListFragment : 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 ->
// Update the UI
}
}
}
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 ->
// Update the UI
})
}
}
ㄴ> by activityViewModels() : 두 프래그먼트는 모두 자신이 포함된 액티비티를 검색합니다.
그러면 각 프래그먼트는 ViewModelProvider를 가져올 때 이 액티비티으로 범위가 지정된 동일한 SharedViewModel 인스턴스를 받아 프래그먼트 간 데이터 공유가 가능하다.
ㄴ> 이 접근 방법의 이점
- 액티비티는 아무것도 할 필요가 없거나 이 커뮤니케이션에 관해 어떤 것도 알 필요가 없습니다.
- 프래그먼트는 SharedViewModel 계약 외에 서로 알 필요가 없습니다. 프래그먼트 중 하나가 사라져도 다른 프래그먼트는 계속 평소대로 작동합니다.
- 각 프래그먼트는 자체 수명 주기가 있으며, 다른 프래그먼트 수명 주기의 영향을 받지 않습니다. 한 프래그먼트가 다른 프래그먼트를 대체해도, UI는 아무 문제 없이 계속 작동합니다.
2) ViewModel로 로더 대체하기
ㄴ> ViewModel + Room + LiveData와 함께 작업하여 로더를 대체합니다.
(로더클래스 중 'CursorLoader'는 앱 UI의 데이터와 데이터베이스 간의 동기화를 유지하는 데 자주 사용됩니다.)
ㄴ> ViewModel 은 기기 설정이 변경되어도 데이터가 유지되도록 보장합니다.
데이터베이스가 변경되면 Room 에서 LiveData에 변경을 알리고,
알림을 받은 LiveData는 수정된 데이터로 UI를 업데이트합니다.
[출처]
- https://developer.android.com/topic/libraries/architecture/viewmodel
- https://todaycode.tistory.com/33?category=979455