2018년 2월 19일 월요일

[안드로이드] Architecture Component 1 - ViewModel 공식문서 번역

ViewModel

ViewModel 클래스는 라이프사이클을 고려하여, UI 관련된 데이터를 저장하고 관리하기 위해 설계되었다. ViewModel 클래스는 화면회전과 같이 설정이 변경되는 상황에서도 data가 계속 남아있을 수 있도록 해준다.
  • Note : ViewModel을 프로젝트에 추가하려면 다음 링크를 확인한다.
안드로이드 프레임워크는 ActivityFragment와 같은 UI 컨트롤러의 라이프사이클을 관리한다. 프레임워크눈 사용자의 특정한 동작이나 완전히 예상치못한 장치의 이벤트에 대한 응답으로 UI 컨트롤러를 파괴(Destroy)하고 재생성(re-create)하는 것을 결정하기도 한다.
시스템이 UI 컨트롤러를 파괴 & 재생성하면, 그 안에 저장해두었던 UI 관련 임시데이터들은 모두 사라진다. 예를들면, 앱에는 사용자의 목록이 포함되어 있을 수 있다. 설정이 변경되어 Activity가 재생성될 때, 새로운 Activity 인스턴스는 사용자목록을 다시 불러와야 한다. 단순한 데이터일때는 Activity의 onSaveInstanceState()메서드를 이용하여 Bundle에 저장하고 onCreate()에서 Bundle에 저장된 데이터를 불러올 수 있지만, 이는 데이터의 양이 적고, 해당 데이터가 직렬화/역직렬화가 가능할 때에만 해당하는 방법이다. 잠재적으로 데이터의 양이 많다면 적절한 방법이 아니다.
또 다른 문제점으로는, UI 컨트롤러가 자주 비동기호출을 만들어서, 반환하는데 다소 시간이 걸릴 수 있다. 잠재적인 메모리 누수를 피하기 위해, UI컨트롤러는 Destroy 된 후에 시스템이 instance를 정리하기 전에 비동기 호출을 관리할 필요가 있다. 이러한 관리작업은 엄청난 유지보수를 필요로 하고, 설정변화에 의해 인스턴스가 재생성되는 경우에 이전에 구성했던 데이터들을 버리고 새로 구성해야하기 때문에 리소스가 낭비된다.
Activity, Fragment와 같은 UI 컨트롤러는 주로 사용자에게 UI 데이터를 보여주고, 사용자의 액션에 반응하고, 권한요청과 같이 OS의 요청을 처리하는 용도로 사용된다. 데이터베이스나 네트워크로부터 데이터를 불러오는 동작을 UI 컨트롤러에서 수행하면 클래스의 부피가 커지게 된다. UI 컨트롤러에 과도하게 책임이 걸려있으면 자칫 클래스가 단일화되어 테스트가 매우 어려워 질 수 있다.
ViewModel은 UI 컨트롤러의 로직으로부터 UI 데이터 로직을 분리할 수 있는 매우 효과적인 방법이다.

ViewModel 구현

구글에서 제공하는 Architecture Components에서는, UI 데이터를 직접 준비하는 UI 컨트롤러들을 위해서 ViewModel과 연동할 수 있도록 헬퍼클래스를 제공한다. ViewModel 객체는 앱의 설정변경이 일어나도 데이터를 유지하므로,설정변경 후의 새로운 Activity, Fragment 인스턴스에서도 바로 데이터를 사용할 수 있다. 예를 들어, Activity나 Fragment에서 사용자 목록 화면을 구성해야 한다면, 다음의 예제코드처럼 ViewModel 내부에서 해당 데이터를 불러오고, 유지 관리하도록 구현해야 한다.
public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asyncronous operation to fetch users.
    }
}
이후에, 다음과 같이 Activity에서 그 리스트를 접근하도록 할 수 있다.
public class MyActivity 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.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}
Activity가 재생성되어도 처음의 Activity 인스턴스에서 생성된 MyViewModel 인스턴스와 같은 인스턴스를 접근할 것이다. ViewModel을 소유한 Activity가 종료되면, 프레임워크는 ViewModel 객체의 onCleared() 메서드를 호출할 것이다. 개발자는 그 메서드를 오버라이드하여 리소스를 해제하도록 구현해야 한다.
  • Caution : ViewModel 인스턴스는 반드시 뷰, Lifecycle, Activity 참조를 가지고있는 어떤 클래스도 참조를 유지하면 안된다.
ViewModel 인스턴스는 뷰나 LifecyclerOwners의 특정 인스턴스보다 오래 유지되도록 설계되었다. 이러한 설계는 ViewModel 인스턴스가 뷰나 Lifecycler 인스턴스를 모르기 때문에, ViewModel에 대해 더 쉽게 테스트를 작성할 수 있게 해준다. ViewModel 인스턴스는 LiveData 인스턴스와 같은 LifecycleObservers를 포함할 수 있다. 하지만, ViewModel 인스턴스는 LiveData같이 라이프사이클 기반의 Observable 클래스의 변화를 관찰해서는 안된다. 만약, ViewModel이 시스템 서비스를 찾는 등의 이유로 Application Context가 필요하다면, AndroidViewModel 클래스를 상속받아서 생성자에서 Application 객체를 받도록 구현할 수 있다.

ViewModel의 생명주기

ViewModel의 유효 스코프는 ViewModel 인스턴스를 얻기 위해 ViewModelProvider에게 전달된 Lifecycle로 지정된다. ViewModel 인스턴스는 Lifecycle의 유효스코프가 완전이 끝날때 까지, 메모리에 남아있게 된다. Activity의 경우라면 finish되었을 때이며, Fragment라면 Activity로부터 detach되었을 때 이다.
아래의 그림은 Activity가 화면 회전이 되고, 종료되기 까지의 라이프사이클 상태를 설명하고 있다. 이 그림에서 Activity의 라이프사이클에 연관되어 ViewModel 인스턴스의 생존시간도 확인 할 수 있다. Fragment의 기본 생명주기에 따른 ViewModel의 생존시간도 이와 동일하다.
viewmodel-lifecycle.png
일반적으로 개발자는 ViewModel 인스턴스를 Activity의 첫 시작점인 onCreate()에서 요청할 것이고 onCreate()메서드는 상황에 따라 여러번 호출될 수도 있지만, ViewModel은 최초 요청으로부터 Activity가 소멸될 때까지 메모리에 유지된다.

Fragment간 데이터 공유하기

하나의 Activity안에서 2개 이상의 Fragment 간에 데이터를 주고받으며 소통하는 것이 흔한 경우이다. 에를들어 마스터-디테일 Fragment 구조에서는 마스터에서 목록이 표시되며, 디테일에서는 마스터에서 선택된 항목의 상세 내용이 표시되어야 한다. 이를 구현하려면 각 Fragment는 인터페이스를 구성해야 하며, Activity가 그 인터페이스를 모두 바인드해야 한다. 또한, 모든 Fragment가 상대 Fragment가 생성이 안되었거나 안보이는 등의 예외케이스에 대한 방어처리가 되어있어야 한다.
이러한 흔한 문제들은 ViewModel 을 사용하여 해결할 수 있다. 다음의 예제코드처럼 Fragment들이 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 ->
           // Update the UI.
        });
    }
}
ViewModelProvider를 얻을 때, 두 프레그먼트 모두 getActivity() 메서드를 이용하고 있음을 주의한다. 같은 Activity를 이용하여 같은 ViewModel 객체를 요청하므로, 동일한 인스턴스가 얻어질 것이다.
이러한 접근방법은 다음과 같은 이점이 있다.
  • Activity가 각 Fragment간 데이터 전달시에 추가적인 작업을 할 필요가 없다.
  • 각 Fragment는 ViewModel 외에 다른 객체나 상태에 대해 더 알 필요가 없다. 그러므로 다른 Fragment가 사라지더라도 정상적으로 동작할 것이다.
  • 각 Fragment는 다른 Fragment의 라이프사이클을 신경쓰지 않고, 자신의 라이프사이클대로 작업을 수행할 수 있다.

ViewModel로 Loader 대체하기

CursorLoader 같은 Loader 클래스들은 UI에서 데이터베이스와의 싱크를 유지한 채 데이터를 가지고있기 위해 사용했었다. ViewModel과 몇몇 추가 클래스들을 이용하여 Loader를 대체할 수 있다. ViewModel을 사용하면 UI 컨트롤러와 데이터 로딩 작업을 분리할 수 있다.
Loader를 이용했던 흔한 접근방법중에 하나로, 앱은 데이터베이스의 정보를 관찰하기 위해 CursorLoader를 이용했었다. 데이터베이스에서 값이 변경되면, Loader는 관련 데이터를 자동적으로 가져온 후에 UI를 갱신하는 역할을 담당했다.
viewmodel-loader.png
ViewModelRoomLiveData를 이용하여 Loader를 대체할 수 있다. ViewModel을 이용하여, 디바이스의 구성이 변경되어도 데이터가 살아남도록 할 수 있고, Room은 데이터베이스의 변화가 있을 때 LiveData에 알려주면, LiveData는 새로 받아온 데이터를 이용해 UI를 갱신한다.
viewmodel-replace-loader.png

원문 : https://developer.android.com/topic/libraries/architecture/viewmodel.html#loaders


댓글 없음:

댓글 쓰기