UI 관련 데이터를 수명 주기를 의식한 방식으로 저장하고 관리하도록 설계되었다. 이 ViewModel클래스를 사용하면 화면 회전과 같은 구성 변경 후에도 데이터를 유지할 수 있다.
안드로이드 프레임워크는 액티비티나 프래그먼트의 수명 주기를 관리합니다.
시스템이 UI 컨트롤러를 파괴하거나 다시 생성하면 저장한 일시적인 UI 관련 데이터가 손실됩니다. (화면 회전을 하면 입력했던 정보들이 사라지는 등)
간단한 데이터의 경우, 이 작업은 onSaveInstanceState() 메서드를 사용하여 onCreate()의 번들에서 데이터를 복원할 수 있지만, 이 접근 방식은 사용자 목록이나 비트맵과 같은 잠재적으로 많은 양의 데이터에 적합하지 않고 직렬화 후 역직렬화할 수 있는 소량의 데이터에만 적합하다.
또 다른 문제는 UI 컨트롤러가 반환하는 데 시간이 걸릴 수 있는 비동기식 호출을 자주 수행해야 한다는 것이다. (서버에서 데이터를 가져오는 등) UI 컨트롤러는 이러한 호출을 관리하고 잠재적인 메모리 누수를 방지하기 위해 파괴된 후 시스템이 이를 정리하도록 해야 한다. 이 관리에는 많은 유지 관리가 필요하며 구성 변경을 위해 개체를 다시 만드는 경우 개체가 이미 만든 호출을 다시 발행해야 할 수 있으므로 리소스 낭비이다. (화면이 Destroy 될때마다 메모리를 해제해야 하고 Create 될때마다 다시 생성해야 한다.)
액티비티 및 프래그먼트와 같은 UI 컨트롤러는 주로 UI 데이터를 표시하고, 사용자 작업에 반응하거나, 권한 요청과 같은 운영 체제 통신을 처리하기 위한 것이다.
UI 컨트롤러에 과도한 책임을 할당하면 테스트가 훨씬 더 어려워진다. 여기서 말하는 과도한 책임이란 네트워크에서 데이터를 로드하는 작업까지도 UI 컨트롤러에 책임을 가하는 것이다.
UI 컨트롤러 로직에서 뷰 데이터 소유권을 분리하는 것이 더 쉽고 효율적이다.
예를 들어 앱에 사용자 목록을 표시해야 하는 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.
}
}
그런 다음 다음과 같이 액티비티에서 목록에 액세스할 수 있다.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 시스템이 액티비티의 onCreate() 메서드를 처음 호출할 때 ViewModel을 만듭니다.
// 다시 작성된 액티비티는 첫 번째 액티비티에 의해 작성된 것과 동일한 MyViewModel 인스턴스를 수신합니다.
// 'by viewModels()' Kotlin 속성 대리자 사용
// from the activity-ktx artifact
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
뷰모델은 view, 수명 주기 또는 activity context에 대한 참조를 포함할 수 있는 클래스를 참조해서는 안 됩니다.
ViewModel 개체는 뷰 또는 라이프사이클 소유자의 특정 인스턴스보다 오래 지속되도록 설계되었다. 또한 View Model은 View 및 Lifecycle 객체를 모르기 때문에 View Model을 보다 쉽게 다룰 수 있는 테스트를 작성할 수 있다.
ViewModel 개체에는 LiveData 개체와 같은 LifecycleObserver가 포함될 수 있다.
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
ViewModel 객체의 범위는 ViewModel을 가져올 때 ViewModelStoreOwner가 ViewModelProvider에게 전달한 수명 주기로 지정된다. ViewModel은 ViewModelStoreOwner가 영구적으로 삭제될 때까지 메모리에 남아 있다.
ViewModelProvider.get() 메서드를 사용하면 ViewModelStoreOwner로 범위가 지정된 ViewModel의 인스턴스를 가져올 수 있습니다.
public class MyActivity extends AppCompatActivity {
// The ViewModel is scoped to `this` Activity
MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
}
public class MyFragment extends Fragment {
// The ViewModel is scoped to `this` Fragment
MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
}
kotlin:
class MyActivity : AppCompatActivity() {
// ViewModel API available in activity.activity-ktx
// The ViewModel is scoped to `this` Activity
val viewModel: MyViewModel by viewModels()
}
class MyFragment : Fragment() {
// ViewModel API available in fragment.fragment-ktx
// The ViewModel is scoped to `this` Fragment
val viewModel: MyViewModel by viewModels()
}
class SharedViewModel : ViewModel() {
val selected = MutableLiveData()
fun select(item: Item) {
selected.value = item
}
}
class ListFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
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() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer { item ->
// Update the UI
})
}
}
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.CreationExtras
class MyViewModel(
private val myRepository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ViewModel logic
// ...
// Define ViewModel factory in a companion object
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// Get the Application object from extras
val application = checkNotNull(extras[APPLICATION_KEY])
// Create a SavedStateHandle for this ViewModel from extras
val savedStateHandle = extras.createSavedStateHandle()
return MyViewModel(
(application as MyApplication).myRepository,
savedStateHandle
) as T
}
}
}
}
그런 다음 ViewModel의 인스턴스를 검색할 때 이 팩토리를 사용할 수 있다. 공유도 가능하다.
import androidx.activity.viewModels
class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels { MyViewModel.Factory }
// Rest of Activity code
}
ViewModel이 액티비티나 프래그먼트의 라이프 사이클보다 길기 때문에 상태를 유지하거나 ViewModelScope를 통해 쉽게 코루틴을 사용할 수 있다. 그렇담 어떻게 인지를 하고 해제가 되는 걸까?
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
Closeable의 close()가 불려야 해제가 된다.
public abstract class ViewModel {
// Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
@Nullable
private final Set<Closeable> mCloseables = new LinkedHashSet<>();
private volatile boolean mCleared = false;
ViewModelScope를 Map으로 관리하게 된다. (setTagIfAbsent())
<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
// It is possible that we'll call close() multiple times on the same object, but
// Closeable interface requires close method to be idempotent:
// "if the stream is already closed then invoking this method has no effect." (c)
closeWithRuntimeException(result);
}
return result;
}
이 clear()는 어떻게 불려지게 되는걸까?
액티비티나 프래그먼트가 모두 종료가 되어야 불리기 때문에 ComponentActivity 코드를 확인해 보면
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
라이프 사이클을 확인해서 ON_DESTROY일 때 getViewModelStore().clear()가 호출되는 것을 볼 수 있다. ViewModelStore는 말 그대로 ViewModel이 저장되는 곳이다.
public class ViewModelStore {
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
Set<String> keys() {
return new HashSet<>(mMap.keySet());
}
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}
ViewModelStore 코드를 살펴보면, 갖고있는 ViewModel의 clear()를 호출하는 것을 볼 수 있다.
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
// We need the same null check here
if (mCloseables != null) {
synchronized (mCloseables) {
for (Closeable closeable : mCloseables) {
closeWithRuntimeException(closeable);
}
}
}
onCleared();
}
closeWithRuntimeException(closeable) 내부 코드를 보면
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Closeable의 close()를 호출하는 것을 볼 수 있다.
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
CloseableCoroutineScope은 Closeable을 상속받기 때문에 close()가 호출되어 coroutineContext.cancel()가 실행된다.
이렇게 우리가 job 해제를 고려하지 않고 viewmodelscope를 통해 쉽게 코루틴을 사용할 수 있는 것이다.