# ViewModel

ViewModel 클래스는 라이프 사이클을 고려하여 UI 관련된 데이터를 저장하고 관리하기 위해 설계되었다. 화면 전환과 같이 설정이 변경되는 상황에서도 data가 계속 남아있을 수 있도록 해준다.

안드로이드 프레임워크는 Activity, Fragment와 같은 UI 컨트롤러의 라이프사이클(즉, 생명주기)을 관리한다. 프레임워크는 사용자의 특정한 동작이나 완전히 예상치 못한 장치의 이벤트에 대한 응답으로 UI 컨트롤러를 파괴(Destroy)하고 재생성(re-create)하는 것을 결정하기도 한다.(액티비티가 종료되고 재생성되는 경우가 해당된다.)

시스템이 UI 컨트롤러를 파괴, 재생성하면 그 안에 저장해두었던 UI 관련 데이터들은 모두 사라진다. 예를 들어, 앱에는 사용자의 목록과 같은 데이터가 포함되어 있을 수 있다. 갑자기 설정이 변경되어 Activity가 재 생성될 때, 새로운 Activity 인스턴스는 사용자의 목록을 다시 불러와야 한다.

단순한 데이터를 다룰 때에는 Activity의 onSaveInstanceState() 함수를 이용해 Bundle 객체에 저장하고 onCreate() 함수에서 Bundle 객체에 저장된 데이터를 불러올 수 있다. 하지만, 이 경우에는 bitmap과 리스트 형식의 많은 양의 데이터가 아닌 직렬화, 역직렬화가 가능한 작은 데이터에 적합하다.
즉, 잠재적으로 데이터의 양이 많다면 적절한 방법이 아니다.

또 다른 문제는 UI 컨트롤러가 자주 비동기 호출을 만들어서 반환하는데 다소 시간이 걸릴 수 있다는 것이다. 잠재적인 메모리 누수를 피하기 위해 UI 컨트롤러는 Destroy 된 후에 시스템이 instance를 정리하기 전에 비동기 호출을 관리할 필요가 있다.
이런 관리 작업은 엄청난 유지보수를 필요로 하고, 상태 변화로 인해 객체를 재생성하는 경우, 이미 만들어진 객체를 다시 호출해야 하므로 리소스가 낭비된다.

Activity, Fragment와 같은 UI 컨트롤러는 사용자에게 UI 데이터를 보여주고, 사용자 액션에 반응하고, 권한 요청과 같은 OS의 요청을 처리하는 용도로 사용된다.

데이터베이스나 네트워크로부터 데이터를 불러오는 동작은 UI 컨트롤러에서 수행하면 클래스의 크기가 커지게 된다. 따라서 UI 컨트롤러에 과도한 책임이 할당되면 클래스가 단일화되어 테스트가 매우 어려워질 수 있다.

그래서 ViewModel을 사용함으로써 UI 컨트롤러의 로직으로부터 UI 로직과 데이터 소유권을 분리할 수 있는 매우 효과적인 방법이다.

# ViewModel 예시.

Google의 Android Architecture Components(이하 AAC)에서 UI 컨트롤러들을 위한 헬퍼 클래스인 ViewModel을 제공한다.
ViewModel 객체는 자동으로 화면 회전 같은 상태 변화동안 자동으로 유지되고 새로운 액티비티 또는 프래그먼트에서도 데이터를 즉시 사용할 수 있다.

MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainViewModel extends ViewModel{
private MutableLiveData<List<User>> userList;
public LiveData<List<User>> getUsers(){
if(userList == null){
userList = new MutableLiveData<List<User>>();
loadUsers();
}
return userList;
}

private void loadUsers(){
// Do an asynchronous operation to fetch userList.
}
}

MainActivity

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends AppCompatActivity{
public void onCreate(Bundle savedInstanceState){
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.

MainViewModel viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
viewModel.getUsers().observe(this, userList ->{
// update UI
})
}
}

만약 액티비티가 재생성이 된다면 처음 만들어진 MainViewModel 인스턴스를 받게 된다. 이 인스턴스를 호출한 액티비티 또는 프래그먼트가 destroy 되어 메모리 해제가 되기 전까지 데이터를 유지하고 있어서 데이터를 보관하고 있다가 화면 회전 같은 상태 변화가 발생해도 데이터를 유지하게 된다.

안드로이드 프레임워크는 내부적으로 class, ViewModel의 Map을 관리하므로 이미 존재하는 ViewModel의 레퍼런스를 가져올 때마다 그 인스턴스를 반환하게 된다.

ViewModel을 소유한 액티비티가 종료되면 프레임워크는 자동으로 ViewModel 객체의 onCleared() 메소드를 호출할 것이다. 그렇다면 우리는 이 함수를 오버라이드하여 리소스를 해제하면 된다.

ViewModel 인스턴스는 뷰나 LifecycleOwners의 특정 인스턴스보다 오래 유지되도록 설계되었다. 이런 설계는 ViewModel 인스턴스가 뷰나 Lifecycle 인스턴스를 모르기 때문에 ViewModel에 대해 더 쉽게 테스트를 작성할 수 있게 해준다.

ViewModel 인스턴스는 LiveData 인스턴스와 같은 LifecycleObservers를 포함할 수 있다. 하지만, LiveData와 같은 라이프사이클 기반의 Observable 클래스의 변화를 관찰해서는 안된다. 만약, ViewModel이 시스템 서비스를 찾는 등의 이유로 Application Context가 필요하다면, AndroidViewModel 클래스를 상속받아서 생성자에서 Application 객체를 받도록 구현할 수 있다.

  • 주의 : ViewModel 인스턴스는 반드시 뷰, Lifecycle, Activity 참조를 가지고 있는 어떤 클래스도 참조를 유지하면 안된다. 이유는 메모리 누수가 발생할 수 있기 때문이다.

# ViewModel 생명주기.

ViewModel의 생명주기는 ViewModelProvider에 전달된 Lifecycle에 생명주기 범위가 지정된다.
즉, 주어진 액티비티가 살아있는 동안 ViewModel 객체는 메모리에 계속 남아있는다.
액티비티의 경우에는 finish될 때, 프래그먼트의 경우에는 액티비티로부터 detached될 때까지이다.

위의 그림에서 ViewModel의 스코프를 확인할 수 있다. 그림은 Activity가 화면 회전되었을 때의 생명주기를 나타낸다. 화면이 회전해도 상태 변경이 되는 상황에서도 살아 있음을 보여준다. Fragment의 기본 생명주기에 따른 ViewModel의 생존시간도 동일하다.

일반적으로 ViewModel 인스턴스를 Activity의 시작점인 onCreate()에서 요청할 것이다. onCreate() 함수는 상황에 따라 여러 번 호출될 수 있지만, ViewModel 객체는 최초 요청부터 Activity가 소멸될 때까지 메모리에 유지된다.

# 의문점?!

A 액티비티에서 ViewModel 객체를 생성하면 scope는 A 액티비티의 생명주기를 따르낟. 만약, B 액티비티에서 ViewModel에 저장된 값을 재사용하고 싶다면??

B 액티비티에서 ViewModelProviders로 객체를 다시 구해오면 A 액티비티에서 만든 객체가 아닌 새로운 객체를 만들어낸다.

how to reuse the same ViewModel on different activites

위의 글을 보면 싱글톤 팩토리로 동작하는 custom ViewModel Factory를 전달하여 다른 액티비티에서도 동일한 ViewModel 인스턴스를 받을 수 있는 방법이 있다.
하지만, 다른 액티비티의 생명주기에서 ViewModel 객체를 유지하는 것은 안티패턴이다.

RxJava와 다르게 생명주기에 따라 데이터를 보관/관리 해주는 LiveData의 장점을 버리는 방식이라고 생각된다.

따라서 ViewModel의 객체를 유지시키는 것이 아닌 DataSource나 Repository를 싱글톤으로 유지하는 것이 더 추천되는 방식이다.
https://stackoverflow.com/questions/49364550/

ViewModel의 객체를 유지시키는 방식이 아닌 Other data layers(data source, data source를 관리하는 Repository)를 싱글톤 객체로 만들어 데이터를 유지시켜 다른 Activity들에서 새로운 ViewModel 객체를 만들어 Repository를 통해 보관중이 데이터를 가져오는 방식이다.

# Fragment 간 데이터 공유.

하나의 액티비티 안에서 2개 이상의 프래그먼트 간에 데이터를 주고 받는 구조는 흔한 경우다. 이럴 때는 프래그먼트의 scope를 사용하는 것이 아닌 프래그먼트들을 감싸고 있는 액티비티의 scope를 전달하면 된다.

즉, 프래그먼트들이 액티비티 scope의 ViewModel을 서로 공유하도록 구현하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

public void select(Item item) {
selected.setValue(item);
}

public LiveData<Item> getSelected() {
return selected;
}
}

public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}

public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}

ViewModelProvider를 얻을 때, 두 프래그먼트 모두 getActivity() 메소드를 이요하고 있다. 같은 Activity를 이용하여 같은 ViewModel 객체를 요청하므로 동일한 객체가 얻어진다.

이 접근 방법은 다음과 같은 이점이 존재한다.

  • Activity가 각 Fragment간 데이터 전달 시에 추가적인 작업을 할 필요가 없다.
  • 각 Fragment는 ViewModel 외에 다른 객체나 상태에 대해 더 알 필요가 없다. 그러므로 다른 Fragment가 사라지더라도 정상적으로 동작할 것이다.
  • 각 Fragment는 다른 Fragment의 라이프사이클을 신경쓰지 않고, 자신의 라이프사이클 대로 작업을 수행할 수 있다.

# 참고