[Android] [번역] ViewModels and LiveData

Jay·2021년 1월 14일
1

Android

목록 보기
5/39
post-thumbnail
post-custom-banner

Why ViewModel and LiveData

MVVM 패턴을 쓰게 되면서 뷰모델과 Livedata를 결합하여 많이 사용하게 된다.
모든 기술이 그렇듯 장단점이 존재하고 최적의 조합으로 구성하여 알맞은 환경에서 사용하여야 효율을 극대화 할 수 있다.
그래서 오늘은 뷰모델과 라이브 데이터를 쓰는 좋은 패턴과 좋지 않은 패턴에 대해 적어보고자 한다.

(번역 및 각종 자료에 대한 글입니다. 오번역 및 잘못된 점에 대한 지적은 늘 환영입니다.)


Views and ViewModels

책임을 나누자.(전가하자?)

가장 이상적으로는, ViewModels는 안드로이드의 어느것도 알 필요가 없다.
그러한 점은 테스트를 용이하게 해주고, 메모리 누수를 막을 수 있고, 모듈화 하기 용이하다. 경험상 일반적인 규칙은 ViewModels에 "android."와 같은 것을 import하는지 확인하는 것이다.(android.arch. 와 같은 예외 포함).


절대 ViewModels에게 Android Framework를 알려주지 마라.

조건문, 루프 및 일반적인 부분은 Activity 또는 Fragment가 아닌 ViewModels 또는 앱의 다른 계층에서 수행되어야 한다.
View는 일반적으로 단위 테스트를 거치지 않기에 코드를 줄이면 줄일 수록 좋은 부분이다.
View는 data를 어떻게 보여줄지와 사용자 이벤트를 ViewModel로 보내는 방법만 알고 있으면 된다. 이를 Passvie View라고 부른다.


Activity와 Fragment 내의 로직을 최소화 하자.

ViewModesl에서의 View 참조.

ViewModels는 Activity & Fragment와는 다른 scope를 갖고 있다.
ViewModel이 살아 있는 동안에, Activity는 Activity lifecycle의 어느 상태에든 있을 수 있다. ViewModel이 인지하지 못하는 동안에, Activity와 Fragments는 destroy될 수 있으며 재창조(재시작) 될 수 있다.

View 참조에 대해서 ViewModel에 전달하는 것은 매우 위험한 문제이다.🤯
예를 들어, 뷰모델에서 네트워크로부터 데이터를 요청하고 일정 시간뒤에 데이터를 받는다고 생각해보자.
그 순간에, View 참조가 사라지거나(Destroy) 그렇기에 더 이상 표현이 어려운 이전의 활동이라면 메모리 누수를 발생 시킬 수 있다.😭

❌ ViewModels에서 View에 대한 참조를 피하자.

ViewModels과 Views가 통신하기 위해 사용하는 방법 중 추천할 수 있는 방법은 Livedata를 사용한 옵저버 패턴이나 다른 library로부터 observable하게 하는 것이다.


Observer Pattern

Android의 Presentation 계층을 디자인하기 가장 편리한 방법은 뷰(Activity, Fragment)가 뷰모델을 관찰하게 만드는 것이다. (변화에 대한 구독이겠지.❗️)
ViewModel이 Android에 대해 모르기 때문에 얼마나 뷰를 많이 죽이는지(destroy) 모르게 된다. 이 부분에서 장점을 찾을 수 있다.⭕️

  1. ViewModels는 화면 회전과 같이 구성이 변경되더라도 유지가 되기에, db나 network에 데이터를 재요청할 필요가 없다.

  2. 장기적인 작업이 끝났을 때, 뷰모델의 observable은 갱신된다. 데이터가 관찰 가능한지 아닌지에 대해선 상관없다. 존재하지 않는 뷰에 대해 업데이트를 하려 하는 Nullpointer Exception은 더 이상 발생하지 않는다.👍

  3. ViewModels는 뷰를 참조하지 않는다. 그렇기에 메모리 누수가 발생하지 않는다.

아래는 전형적인 방식의 Activity,Fragment에서의 구독 방법이다.

private void subscribeToModel() {
  // Observe product data
  viewModel.getObservableProduct().observe(this, new Observer<Product>() {
      @Override
      public void onChanged(@Nullable Product product) {
        mTitle.setText(product.title);
      }
  });
}

✅ UI에게 데이터를 던저주지 말고, UI가 변경을 감지하게 하자.

Fat ViewModels

(⬆️ 이거 너무 웃긴 소제목 같다..)

어떤것이든 관심사의 분리는 좋은 것이다. 만약 ViewModel이 너무 많은 코드를 갖고 있거나 많은 책임을 갖고 있다면? 이것을 고려해보자.

  • ViewModel과 동일한 Scope를 갖는 Presenter에게 일부 로직을 옮겨보자. 그러면 app의 다른 부분과 통신하며 ViewModel을 통해 Livedata를 갱신할 수 있다.
  • Domain Layer를 추가하여 클린 아키텍처를 적용해보자.😅 그럼 테스트 용이하고 유지보수가 쉬운 아키텍처를 만들어줄것이다. 이것은 메인 스레드를 빠르게 만들어줄 수도.. 오랜 예제지만 클린 아키텍처 샘플 코드이다.

✅ 필요하다면 domain layer를 추가해서 책임을 분산시키자.

Data Repository를 사용해보자.

앱 아키텍처 가이드를 보면 다수의 data sources를 갖고 있다.
1. Remote : network or cloud
2. Local : db or file
3. In-memory cache

Data layer를 갖게 되는 것은 Presentation Layer가 아예 모르게 하는 좋은 방법이다. 캐시와 DB를 네트워크와 동기적으로 유지하는 것은 쉽지 않다. 이러한 복잡함에 대해 단일 지점으로 분리된 repository를 갖는 것은 좋은 방법이라고 생각된다.

🥺 혹시 다수의 다른 데이터 모델을 갖고 있다면, 다수의 repository를 갖는걸 추천한다.


Data Access에 대한 단일지점으로 Data repository를 추가해라.

Data의 상태를 다루자.

예를 들어보자.
화면에 보여지는 리스트 형태의 데이터를 갖고 있는 ViewModel에 의해 노출된 LiveData를 옵저빙하고 있다고 생각해보자.
View는 로드 중인 데이터, 네트워크 오류, 빈 리스트를 어찌 구분할 수 있을까?

  • 아마 뷰모델로부터 LiveData<MyDataState> 를 노출할 수 있다.
    예를 들어, MyDataState에는 데이터가 로드중인지, 성공적으로 로드되었는지 실패되었는지에 대한 정보들이 포함될 수 있다.

    이렇게 데이터의 상태와 에러 메세지와 같은 다른 메타 데이터를 wrap할 수 있다.
    이러한 부분은 Resource 처리 샘플을 보자. 아주 잘~ 나와 있다.🤗

Activity의 상태를 저장하자.

액티비티의 상태는 액티비티가 사라지거나 프로세스가 종료된 경우, 화면을 다시 만드는데 필요한 중요한 정보이다. 화면 회전과 같은 경우가 가장 명백한 경우이다. 우린 이러한 경우를 ViewModel로 커버할 수 있다. ViewModel에 상태를 보관한다면 안전하니까.😼

그러나, ViewModel 또한 사라진다면 재저장할 필요가 있다. (OS에서 우리의 프로세스를 죽여버린경우...)

효율적으로 저장하고 UI상태를 복구하는 방법은 persistence의 조합을 사용하는 것. onSaveInstanceState()와 뷰모델이다.
관련 내용은 여길 참고하자.


Events 처리

이벤트는 무언가가 일어날 때의 처리다. ViewModel이 데이터를 내보낸다. 그럼 어떤 이벤트가 날까? 예를 들어, Navigation 이벤트 또는 스낵바를 보여주는 이벤트는 한번만 보여져야 한다.

Event에 대한 개념은 LiveData가 어떻게 저장되고 재저장되는 것과 완벽하게 맞지는 않다.

LiveData<String> snackbarMessage = new MutableLiveData<>();

Activity가 옵저빙하기 시작하고 뷰모델은 동작을 끝낸다. 그럼? 메세지가 업데이트 되어야겠지?

snackbarMessage.setValue("Item saved!")

액티비티는 값을 받고 스낵바를 보여준다.

그러나, 화면 회전을 한다면? 새로운 액티비티가 그려지고 옵저빙을 시작한다.
LiveData가 관찰을 시작한다면, 액티비티는 즉시 이전 값을 받을 것이다. (결국 한번 더 보여지는 셈이 된다😭)

이 문제에 대해서, Architecture Components를 Extension을 통한 해결을 하기 이전에, 디자인 문제에 직면한다.
결론은, 이벤트를 상태의 일부로 취급하는 것이 좋다.

디자인 이벤트를 상태의 일부로 보자. 더 자세하게는 이걸 읽어보자. SingleLiveEvent case


뷰모델의 누수

안드로이드에서 반응형 다이어그램은 잘 동작한다. 왜? 어플의 다른 계층들과 UI간에 편리한 연결을 제공해주기 때문에!
LiveData는 이런 부분에서 가장 핵심 요소이다.

ViewModels이 다른 요소들과 얼마나 잘 통신하는지는 개발자 스스로에게 달려있다. 그렇기에 누수와 엣지 케이스를 주의하자.
Presentation layer에서 옵저버 패턴을 사용하고 있고 Data Layer에서 콜백을 받는다고 생각해보자.

유저가 어플을 나간다면, 뷰는 사라지고 뷰모델은 더이상 옵저빙 되지 않는다.
만약 Repository가 싱글톤이거나 Application Scope를 가진 경우, 프로세스가 종료될 때 까지 Reppsitory는 삭제되지 않는다.
만약 repository가 뷰모델의 콜백에 대한 참조를 보유하고 있는 경우, 뷰모델은 일시적으로 누수될 것이다.

뷰모델이 가볍거나 작동을 빨리 끝낼 수 있다는게 보장된다면 큰 문제는 아니다.
그러나, 늘 그렇지 않다. 이상적으로는, ViewModels은 관찰하는 뷰가 없을때마다 자유로워질 수 있어야한다.

여러 방법으로 이를 만족할 수 있다.

  • ViewModel.onCleared()를 통해, 레포지토리에 뷰모델 콜백을 중단시키는 것이다.
  • 레포지토리에서 WeakReference를 사용하거나 Event Bus를 쓸 수 있다.(이 방법은 그리 추천하지는 않는다)
  • Repository와 ViewModel이 통신할 수 있는 LiveData를 사용하는 것이다. (마치 뷰와 뷰모델이 LiveData를 사용하듯이)

✅ 엣지케이스, 누수 및 장기 실행 작업이 아키텍처의 인슽턴스에 미치는 영향을 고려해보자.
❌ clean state를 저장하는데 중요한 데이터와 관련 로직을 뷰모델에 넣지 말자. 뷰모델에서의 모든 호출이 마지막이 될 수 있다.


LiveData in repositories

뷰모델을 콜백 지옥에서 벗어나게 해주기 위해, repository는 아래와 같이 옵저빙 해야한다.

뷰모델이 clear되거나 뷰의 lifecycle이 끝난 경우, 구독은 끝나게 된다.


이러한 방식을 하면 문제가 생긴다. LifecycleOwner에 대한 엑세스 권한 없는 경우라면 ViewModel에서 Repository를 어찌 구독할까?
Transformations가 그에 대한 답이다.
Transformations.swichMap은 다른 LiveData 인스턴스의 변화에 대응하여 새로운 LiveData를 생성할 수 있게해준다. 또한, Lifecycle 정보를 옵저빙할 수 있게 해준다.

LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
        if (repoId.isEmpty()) {
            return AbsentLiveData.create();
        }
        return repository.loadRepo(repoId);
    }
);

위의 경우, 트리거가 업데이트 되면 함수가 적용되고 결과가 다운 스트림으로 전달 된다. 액태비티는 repo를 관찰하고 동일한 LifecycleOwner가 repository.loadRepo(id) 호출에 사용된다.

✅ 뷰모델에서 Lifecycle이 필요하다고 생각 될때마다 Transformation은 해결책이 될 수 있다.


LiveData extend

LiveData의 가장 흔한 사용 케이스는 MutableLiveData이다.
이를 LiveData형태로 내보내고 옵저버로부터 불변하도록 만든다.

더 많은 기능이 필요한 경우, LiveData extend는 활성 옵저버가 있을 때 알려준다. 이건, location or sensor service같은 경우 도움이 된다.

public class MyLiveData extends LiveData<MyData> {

    public MyLiveData(Context context) {
        // Initialize service
    }

    @Override
    protected void onActive() {
        // Start listening
    }

    @Override
    protected void onInactive() {
        // Stop listening
    }
}

When not to extend LiveData.

onActive()를 사용하여 데이터를 로드 하는 일부 서비스를 사용할 수 있지만, 정당한 이유가 없는 한 LiveData가 관찰 될 때까지 기다릴 필요는 없다.

  • ViewModel에 start() 메서드를 추가하고 최대한 빠르게 이를 부르자.
    여기 처럼

  • 로드를 시작하는 프로퍼티를 설정하자.
    여기 처럼

❌ 대개는 LiveData를 extend할 일이 없을 것이다. 액티비티와 프레그먼트에서 뷰모델에 데이터 로드해달라고 말하는게 좋을 것이다.

Reference

  • Thanks Jose Alcérreca for this guidance
profile
developer
post-custom-banner

0개의 댓글