Android/학습

[Android] Jetpack ViewModel

한때미 2024. 11. 14. 03:33

ViewModel

비즈니스 로직 또는 화면 수준 상태 홀더로,

UI 컨트롤러의 데이터를 캡슐화하여 구성 변경이 일어나도 데이터를 유지하는 것이 목적인 구성요소이다.

 

즉, UI에 상태를 노출하고 관련 비즈니스 로직을 캡슐화

 

 

 

ViewModel이 아닌 일반 Class로 UI를 상태를 관리하면 어떻까?

이는 활동(Activity)이나 탐색 대상(Navigation destinations) 간에 이동할 때 문제가 될 수 있습니다. 이렇게 하면 인스턴스 상태 저장 메커니즘을 사용하여 데이터를 저장하지 않을 경우 해당 데이터가 소멸됩니다.

 

참고적으로, ViewModel은 Hilt 및 Navigation과 같은 주요 Jetpack 라이브러리와 Compose와의 통합을 완벽하게 지원

 

 


 

ViewModelStoreOwner

ViewModel의 범위는 ViewModelStoreOwner의 생명주기이며, 

ViewModelStoreOwner가 영구적으로 사라질 때까지 메모리에 남아 있다.

 

 

ViewModel을 인스턴스화할 때 ViewModelStoreOwner interface를 구현하는 객체를 전달.

 

 

참고로, ViewModelStoreOwner interface의 직접 서브 클래스는 다음과 같다.

ComponentActivity, Fragment, NavBackStackEntry

 

> 그 밖의 간접 하위 클래스 등의 정보 페이지

 

ViewModelStoreOwner  |  Android Developers

androidx.appsearch.builtintypes.properties

developer.android.com

 

 

 

즉, ViewModel은 인스턴스가 생성될 때 해당 컴포넌트의 ViewModelStoreOwner 객체를 전달받아 해당 컴포넌트의 수명주기에 의존적이면서 생명주기를 고려하여 동작하기 위해 만들어진 구성요소인 것이다.

 

 

 

AppCompatActivirty의 hierarchy, ComponentActivity를 상속 받고 있음

 

ViewModelStoreOwner를 상속 받아 구현하고 있는 Fragment 구현체

 

 

ViewModel의 범위가 지정된 프래그먼트나 액티비티가 파괴되면, 해당 범위가 지정된 ViewModel에서 비동기 작업이 계속됩니다. 이것이 지속성의 핵심입니다.

개발문서에 적혀있는 말인데 상당히 중의적이라서 취소선으로 표시했다.. 여기서 발하는 비동기 작업이 계속 된다~ 라는 의미는 viewModelScope의 close 등의 작업이 이어진다고 이해했다.

 


 

UI 상태 유지 기법, SavedStateHandle와 차이점은?

 

savedStateHandle는 구성변경뿐만 아니라 프로세스 전반에 걸쳐서도 데이터를 유지할 수 있다. 즉, 사용자가 앱을 닫았다가 나중에 열더라도 UI 상태를 그대로 유지할 수 있는 것이 특징이다.

 

 

비즈니스 로직에 액세스

ViewModel은 UI 레이어의 비즈니스 로직을 처리하기에 적합한 위치에 있으며,

이벤트를 처리하고, 애플리케이션 데이터를 수정하기 위해 비즈니스 로직을 적용해야 할 때 이를 계층 구조의 다른 레이어에 위임하는 역할을 한다. 이를 비즈니스 로직에 대한 액세스 권한을 제공한다고 볼 수 있다.

대부분의 비즈니스 로직이 데이터 레이어에 있지만 UI 레이어에도 비즈니스 로직이 포함될 수 있다.
화면 UI 상태를 만들기 위해 여러 저장소의 데이터를 결합하거나 특정 유형의 데이터에 데이터 레이어가 필요하지 않은 경우

 

 

 

여기서 정리를 하면 ViewModel의 주요 이점은 기본적으로 다음과 같다.

 

ViewModel의 이점

  • UI 상태유지할 수 있습니다.
  • 비즈니스 로직에 대한 액세스 권한을 제공합니다.

 


Jetpack Compose와 함께 사용할 경우 유의할 점

Composeable은 ViewModelStoreOwner가 아니기 때문에  ViewModel의 범위를 컴포저블로 지정할 수 없다

 

하나의 compose에 종속된 viewModel 생명 주기를 가질 수 없기 때문에 예상하지 못하는 동작 발생 가능성이 있다.

예를 들어, 해당 경우는 동일한 ViewModel의 인스턴스를 수신한다.

  • 컴포지션에 있는 두 개의 동일한 composable의 인스턴스
  • 동일한 ViewModelStoreOwner를 가진 동일한 viewModel을 액세스 하는 서로 다른 composable

컴포지션 도식화

어떻게?

Compose에서 ViewModel의 이점을 얻으려면 프래그먼트나 활동(Activity)에서 각 화면을 호스팅 하거나, Compose Navigation을 사용하고 탐색 대상에 최대한 가깝게 구성 가능한(composable) 함수에서 ViewModel을 사용해야 한다.

이는 ViewModel의 범위를 탐색 대상, 탐색 그래프(Navigation), 활동(Activity), 프래그먼트(Fragment)로 지정할 수 있기 때문이다.

 

 

 


ViewModel의 생명 주기

ViewModel의 수명 주기는 범위와 직접 연결된다. 

이때 범위는 ViewModel을 인스턴스화할 때 전달받은 ViewModelStoreOwner가 된다.

 

ViewModel은 범위로 지정된 ViewModelStoreOwner가 사라질 때까지 메모리에 남아 있다. 

 

ViewModelStoreOwner가 사라질 때는 다음과 같다.

  • 활동(Activity)의 경우 완료(finish)될 때입니다.
  • 프래그먼트(Fragment)의 경우 분리(detach)될 때
  • 탐색 항목(Navigation entry)의 경우 백 스택(back stack)에서 삭제(removed)될 때

Activity에서 화면 회전이 발생했을 때 Activity와 ViewModeldml 생명 주기

 

ViewModel은 구성 변경에서도 데이터를 유지할 수 있으며 훌륭한 솔루션이다.

 

ComponentActivity의 구현 일부, 구성변경 발생(isChangingConfigurations) 시 viewModel clear가 예외로 동작한다.

 

참고로, ViewModelStoreOwner interface는 ViewModelStore 자료형의 viewModelStore 변수만을 가지고 있는 interface이다.

 

 

시스템은 활동 기간 내내(예: 기기 화면이 회전될 때) onCreate() 메서드를 여러 번 호출할 수 있으며, 

ViewModel은 처음 ViewModel을 요청할 때부터 활동(Activity)이 완료되고 소멸될 때 존재한다.

 

그렇기 때문에 ViewModel에서 View에 관련된 참조는 일어나서는 안된다.

ViewModel이 의존한 UI 객체의 생명주기 보다 길기 때문에, 해당 객체가 null 익셉션이 발생하기도 한다.

주의: 일반적으로 ViewModel은 뷰, Lifecycle, 또는 활동 컨텍스트(Activity Context) 참조를 보유할 수 있는 클래스를 참조해서는 안 된다. ViewModel 수명 주기가 UI 수명 주기보다 크므로 ViewModel에 수명 주기 관련 API를 보유하면 메모리 누수가 발생할 수 있다.

 


 

lifecycle version 2.5부터

ViewModel의 인스턴스가 제거되면 자동으로 닫히는 Closeable objects를 생성자에 전달할 수 있게 되었다.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}
@MainThread
    internal actual fun clear() {
        impl?.clear()
        onCleared()
    }

 

ViewModel의 clear()에서 인스턴스가 제거될 때 정리해 주는 모습을 확인할 수 있다.

 

 


 

ViewModel 주요 권장사항

칩 그룹(chip groups)이나 양식(forms)과 같은 재사용 가능한 UI 구성요소의 상태 홀더

그렇지 않으면 칩당 명시적 뷰 모델 키를 사용하지 않는 한 동일한 ViewModelStoreOwner 아래에서 동일한 UI 구성 요소의 다른 사용에서 동일한 ViewModel 인스턴스를 얻게 된다.

ViewModel는 범위를 가지기 때문에 화면 수준 상태 홀더의 구현 세부 정보로 사용한다.

 

UI 구현 세부정보

ViewModel API가 노출하는 메서드의 이름과 UI 상태 필드의 이름을 최대한 일반적으로 유지하자.

그러면 ViewModel이 휴대전화, 폴더블, 태블릿 또는 Chromebook과 같은 모든 유형의 UI를 수용할 수 있다.

 

수명 주기 관련 API의 참조(예: Context)

ViewModel은 ViewModelStoreOwner보다 오래 지속될 수 있다.

ViewModel은 메모리 누수를 방지하기 위해, Context나 Resources 같은 수명 주기 관련 API의 참조를 보유해서는 안 된다. 

 

ViewModel을 다른 클래스, 함수 또는 기타 UI 구성요소에 전달 ❌

플랫폼에서 이를 관리하므로 최대한 플랫폼에 가깝게 유지해야 한다.

즉, Activity, fragment 또는 화면 수준의 구성 가능한 함수에 가깝게 유지해야 한다.

이렇게 하면 하위 수준의 구성 요소가 필요 이상으로 많은 데이터와 로직에 액세스 하는 것을 방지할 수 있다.

 

 

 

 


To Be Continued

  • ViewModel Factory
  • by viewModels()
  • ViewModelStoreOwner

참고 자료

https://developer.android.com/topic/libraries/architecture/viewmodel

 

ViewModel 개요  |  Android Developers

ViewModel을 사용하면 수명 주기를 인식하는 방식으로 UI 데이터를 관리할 수 있습니다.

developer.android.com