ViewModel 개요

ViewModel은 UI와 관련된 data를 생명주기를 신경쓰면서 저장하거나 관리하기위해서 설계된 클래스이다.
ViewModel은 screen rotation 과 같은 configuration변화에서도 살아남는다.
안드로이드 프레임워크는 액티비티나 프레그먼트같은 UI컨트롤러의 생명주기를 관리한다.
안드로이드 프레임워크는 사용자의 행동이나 또는 개발자가 제어할 수 없는 어떤 이벤트(메모리부족 등)로 인해, 아마 UI컨트롤러를 파괴하거나 재생성하게 된다.
만약 시스템이 UI컨트롤러를 파괴하거나 재생성한다면 UI와 관련된 일시적인 data들은 아마 사라지고 말것이다. 예를 들면, UI컨트롤러에서 User라는 클래스의 List를 관리 하고 있는데 가로세로 화면전환으로 인해 액티비티가 재성성된다면 새로 생성된 UI컨트롤러는 이 데이터를 다시 새로 가지고 와야 한다. 예를들면 보통 onSaveInstanceState() 메소드를 통해 번들로부터 데이터를 복구 하게 된다. 하지만 이런 접근 방식은 아주 작은 data에나 적합하지 bitmap이나 사용자 목록 같은 양이 많은 data에는 잠재적으로 알맞지 않다.
또 다른문제가 있다. UI컨트롤러는 종종 비동기적으로 메소드를 호출하고 반환값을 받곤 하는데, 콜백을 받으려고 할때 UI컨트롤러가 박살났다면? 에러가 발생하거나 메모리 누수가 나기 쉽다. 이런거까지 개발자가 다 관리할려면
에휴.. 한숨밖에 안나온다.  그리고 UI컨트롤러가 재생성될때마다 데이터를 다시 얻으려고 하는것도 리소스 낭비이다.
UI컨트롤러는 주로 UI data를 보여주고, 사용자의 행동에 반응하고, 또는 Runtime권한요구와 같은 운영체제와 소통하는것을 다루는 경향이 있다.
데이터베이스나 네트워크로부터 data를 요청하고 반응하는것 까지 UI컨트롤러에서 다 하다보면 마침내 거대해진 액티비티나 프레그먼트를 볼 수 있다. 문제는 이러한 UI컨트롤러의 비대성이 계속 될 것이라는거다…(공포)

UI컨트롤러에게 지나치게 많은 책임감을 부여하는것의 결과는 앱의 모든 동작을 하나의 클래스에게 맡기게 되게 되는셈이다. 그런결과로 단위 테스트가 거의 불가능하게 된다.

ViewModel 구현하기

Architecture Components는 ViewModel을 제공하고 있다. ViewModel은 UI컨트롤러와 상호작용하며 UI에 필요한 data를 준비하는데 쓰인다.
ViewModel객체는 configuration changes(환경변화) 에서도 자동으로 유지가 된다. 그래서 ViewModel이 들고 있던 data들도 재생성된 UI컨트롤러에서 바로 사용가능하다.
예를들면, 만약 사용자목록을 개발자가 보여주고 싶다면 사용자 목록을 얻는 행위는 ViewModel에게 위임하고 UI컨트롤러는 ViewModel로부터 데이터를 받아 사용하면 된다.
다음 샘플 코드를 확인하자

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }
    private void loadUsers() {
        // 이곳에서 사용자목록을 비동기적으로 갖고 오면된다.
    }
}

그럼 액티비티나 프레그먼트에서는 ViewModel의 data에 어떻게 접근할까?

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // 먼저 여기서 ViewModel을 생성한다, 보통 onCreate에서 하면된다.
        // 액티비티가 재생성되더라도 처음 액티비티를 생성했을때 얻었던 ViewModel객체와 같은 객체로 얻을 수 있다
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // 여기서 UI를 업데이트하면된다.
        });
    }
}

만약 액티비티가 재생성되면 해당 액티비티가 가지고 있는 ViewModel은 액티비티가 finished 되기전까지는 같은 객체이다. 안드로이드 프레임워크에서는 이때 ViewModel의 onCleared()메소드를 호출하여 리소스를 정리할 수 있게 도와준다.

주의사항: ViewModel은 절대로 Activity나 Fragment같은 Lifecycle을 참조하면 안된다.

ViewModel객체는 LifecycleOwner나 특정 View보다 오래 살아남을 수 있도록 설계 되었다. 이러한 방식은 ViewModel에 대해서 View와 Lifecycle객체에 대해 모르더라도 테스트 코드 작성을 쉽게 한다.
ViewModel객체는 LiveData객체와 같은 LifecycleObserver를  포함할 수 있다. 하지만 ViewModel은 생명주기를 알고있는 LiveData같은 Observable객체에 대해 observe를 절대로 해서는 안된다.
만약 ViewModel이 Application Context가 필요하다면, 예를들면 system service를 찾기 위해서, AndroidViewModel을 사용하면된다. AndroidViewModel은 생성자에 Application을 인자로 필요로 한다.

ViewModel의 생명주기


ViewModel객체는 ViewModelProvider를 통해 얻을 수 있으며, 이때 Lifecycle객체를 넘겨주게 됨으로서 생명주기를 살필수 있게 된다.
ViewModel은 Lifecycle객체가 영구적으로 사라질때까지 메모리에남아 있게 된다.  여기서 영구적으로 사라진다 함은 액티비티가 종료되거나, fragment가 detached될때를 의미한다.
아래의 그림을 보면 액티비티가 회전할때 시점에 따른 다양한 생명주기 상태를 확인 할 수 있다. 이 그림은 또한 액티비티옆에서 ViewModel의 생명주기도 같이 보여주고 있다. 이러한 특정 다이어그램은 액티비티의 상태를 설명해주고 있고, 프레그먼트의 생명주기도 기본적으로 비슷하게 적용된다.
 
보통 ViewModel을 요청할때 액티비티의 onCreate메소드에서 하게 되는데 system은 액티비티의 생명주기동안 onCreate()를 여러번 호출할수 있고, ViewModel은 액티비티가 파괴될때까지 끝까지 남아 있게 된다.

Fragment사이에서 data 공유하기

두개이상의 프레그먼트를 하나의 액티비티에서 다루며 상호간의 data를 공유하는것은 매우 일반적인 경우이다.
일반적인 Master-detail fragments의 경우를 생각해보자. 하나의 프레그먼트에서는 사용자가 아이템을 선택할수 있고 다른 프레그먼트에서는 해당 아이템의 내용을 보여주게 된다. 이러한 경우 두 프레그먼트에서 interface를 정의하고  이 프레그먼트를 갖고 있는 액티비티는 이 둘을 바인딩 해서 데이터를 주고 받는 그런 작업들이 절대 간단하지가 않다. 게다가 양쪽 프레그먼트 모두 반드시 fragment가 생성되지 않았거나 아직 보이지 않는 다는 점을 고려하여 코드를 작성 해야 한다. 쉽게 말해 데이터를 보여주는것과 관련해서 개발자가 생명주기를 신경써야한다는 것이다.
이런 개발자의 고충은 ViewModel로 해결될  수 있다. 이러한 프레그먼트들은 Activity내에서 ViewModel을 공유함으로 서로 상호작용할 수 있게 된다. 다음 샘플 코드를 확인하자

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 -> {
			//UI 갱신작업
        });
    }
}

뭔가 이제 감이 오는가? 양쪽 모두 getActivity() 호출을 통해 동일 한 액티비티 객체로 ViewModelProvider로 부터 ViewModel을 얻어내고 있다. 그런 결과로 양쪽 프레그먼트모두 같은 SharedViewModel객체를 얻을수 있게 되었다.
이러한 접근 방식은 아래와 같은 장점을 가진다.

  • 액티비티가 이 둘을 위해 뭘 해줘야 하거나 알아야할 게 없다.
  • 프레그먼트는 SharedViewModel 이외의 것들에 대해 알필요가 없습니다. 만약 다른 프레그먼트가 사라진다 하더라도 또 다른 하나의 프래그먼트는 계속해서 동작할 것입니다.
  • 각가의 프레그먼트의 자신의 생명주기를 같습니다 그리고 이것은 다른 프레그먼트의 생명주기에 영향을 미치지 않습니다. 만약 하나의 프레그먼트가 다른것으로 교체된다하더라도 아무문제 없이 UI는 계속해서 작업을 합니다.

Loader를 ViewModel로 교체하기

CursorLoader와 같은 Loader 클래스들은 DB와 UI의 싱크를 맞추기 위해서 종종 데이터를 보관하는데 사용되곤한다. Loader대신에 ViewModel을 사용해보자. ViewModel을 사용하는것으로 UI컨트롤러와 데이터로딩 작업을 분리 할 수 있다. 그러면 클래스간의 강한 참조로부터 해방될 수 있다.
Loader를 사용하는 일반적인 접근방법에서 앱은 아마도 CursorLoader를 사용하고 database의 콘텐츠를 관찰할것이다. 값이 변경될때 Loader는 자동으로 data를 다시 불러오고 UI를 갱신했다.

Loader를 통해서 data를 불러오는 경우

ViewModel은 Room과 LiveData와 같이 동작하며 Loader를 대체 할 수 있다. ViewModel은 data가 기기의 환경변화에도 살아남을 수 있게 보장해준다. Room은 LiveData에게 데이터베이스가 변경될때를 알려주며 LiveData는 그런 변경속에서 변경된 데이터와 함께 UI를 업데이트를 하게 된다.

이 블로그 포스트를 통해 어떻게 ViewModel을 LiveData와 함께 사용하고, AsyncTaskLoader를 대체 할 수 있는지 알 수 있다.

data가 점점 더 복잡해짐에 따라 개발자는 어떻게 데이터를 불러오는 클래스를 분리해야하는지에 대한 고민을 해야한다. ViewModel의 목적은 UI컨트롤러의 환경변화속에서도 data가 살아남을수 있도록 캡슐화하는 것이다.

카테고리: 미분류

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.