ViewModel 개요 Android Jetpack의 구성요소

ViewModel 클래스는 비즈니스 로직 또는 화면 수준 상태 홀더입니다. UI에 상태를 노출하고 관련 비즈니스 로직을 캡슐화합니다. 주요 이점은 상태를 캐시하여 구성 변경에도 이를 유지한다는 것입니다. 즉, 활동 간에 이동하거나 구성 변경(예: 화면 회전 시)을 따를 때 UI가 데이터를 다시 가져올 필요가 없습니다.

상태 홀더에 관한 자세한 내용은 상태 홀더 안내를 참고하세요. 마찬가지로 UI 레이어에 관한 일반적인 자세한 내용은 UI 레이어 안내를 참고하세요.

ViewModel의 이점

ViewModel의 대안은 UI에 표시되는 데이터를 보유하는 일반 클래스입니다. 이는 활동이나 탐색 대상 간에 이동할 때 문제가 될 수 있습니다. 이렇게 하면 인스턴스 상태 저장 메커니즘을 사용하여 데이터를 저장하지 않을 경우 해당 데이터가 소멸됩니다. ViewModel은 데이터 지속성을 위한 편리한 API를 제공하여 이 문제를 해결합니다.

ViewModel 클래스의 주요 이점은 기본적으로 두 가지입니다.

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

지속성

ViewModel은 ViewModel이 보유하는 상태와 ViewModel이 트리거하는 작업에서 모두 지속성을 허용합니다. 이러한 캐싱을 통해 화면 회전과 같은 일반적인 구성 변경에도 데이터를 다시 가져올 필요가 없습니다.

범위

ViewModel을 인스턴스화할 때는 ViewModelStoreOwner 인터페이스를 구현하는 객체를 전달합니다. 이는 탐색 대상, 탐색 그래프, 활동, 프래그먼트 또는 인터페이스를 구현하는 다른 유형일 수 있습니다. 그러면 ViewModel의 범위가 ViewModelStoreOwner수명 주기로 지정됩니다. 이는 ViewModelStoreOwner가 영구적으로 사라질 때까지 메모리에 남아 있습니다.

클래스 범위는 ViewModelStoreOwner 인터페이스의 직접 또는 간접 서브클래스입니다. 직접 서브클래스는 ComponentActivity, Fragment, NavBackStackEntry입니다. 간접 서브클래스의 전체 목록은 ViewModelStoreOwner 참조를 확인하세요.

ViewModel의 범위로 지정된 프래그먼트 또는 활동이 소멸되면 범위가 지정된 ViewModel에서 비동기 작업이 계속됩니다. 이는 지속성의 핵심입니다.

자세한 내용은 아래 ViewModel 수명 주기 섹션을 참고하세요.

SavedStateHandle

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

비즈니스 로직에 액세스

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

ViewModel은 UI 레이어의 비즈니스 로직을 처리하기에 적합한 위치입니다. 또한 ViewModel은 이벤트를 처리하고, 애플리케이션 데이터를 수정하기 위해 비즈니스 로직을 적용해야 할 때 이를 계층 구조의 다른 레이어에 위임하는 역할을 합니다.

Jetpack Compose

Jetpack Compose를 사용할 때 ViewModel은 화면 UI 상태를 컴포저블에 노출하는 기본 수단입니다. 하이브리드 앱에서는 활동과 프래그먼트가 단순히 구성 가능한 함수를 호스팅합니다. 이는 활동 및 프래그먼트로 재사용 가능한 UI 조각을 만드는 것이 그렇게 간단하고 직관적이지 않아 UI 컨트롤러로 훨씬 더 활성화되었던 이전 접근 방식에서 변화된 것입니다.

Compose와 함께 ViewModel을 사용할 때 유의해야 할 가장 중요한 점은 ViewModel의 범위를 컴포저블로 지정할 수 없다는 것입니다. 컴포저블이 ViewModelStoreOwner가 아니기 때문입니다. 컴포지션에서 동일한 컴포저블의 두 인스턴스 또는 동일한 ViewModelStoreOwner에서 동일한 ViewModel 유형에 액세스하는 다른 두 컴포저블은 ViewModel의 동일한 인스턴스를 수신하며 이는 예상된 동작이 아닌 경우가 많습니다.

Compose에서 ViewModel의 이점을 얻으려면 프래그먼트나 활동에서 각 화면을 호스팅하거나, Compose Navigation을 사용하고 탐색 대상에 최대한 가깝게 구성 가능한 함수에서 ViewModel을 사용합니다. 이는 ViewModel의 범위를 탐색 대상, 탐색 그래프, 활동, 프래그먼트로 지정할 수 있기 때문입니다.

자세한 내용은 Jetpack Compose의 상태 호이스팅 가이드를 참고하세요.

ViewModel 구현

다음은 사용자가 주사위를 굴릴 수 있는 화면을 나타내는 ViewModel의 구현 예입니다.

Kotlin

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

그 후에 다음과 같이 활동에서 ViewModel에 액세스할 수 있습니다.

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(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.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

ViewModel과 함께 코루틴 사용

ViewModel에는 Kotlin 코루틴 지원이 포함됩니다. UI 상태를 유지하는 것과 동일한 방식으로 비동기 작업을 유지할 수 있습니다.

자세한 내용은 Android 아키텍처 구성요소와 함께 Kotlin 코루틴 사용을 참고하세요.

ViewModel의 수명 주기

ViewModel의 수명 주기는 범위와 직접 연결됩니다. ViewModel은 범위로 지정된 ViewModelStoreOwner가 사라질 때까지 메모리에 남아 있습니다. 이는 다음과 같은 경우에 발생할 수 있습니다.

  • 활동의 경우 완료될 때입니다.
  • 프래그먼트의 경우 분리될 때입니다.
  • 탐색 항목의 경우 백 스택에서 삭제될 때입니다.

따라서 ViewModel은 데이터를 저장하는 훌륭한 솔루션으로, 구성 변경 후에도 유지됩니다.

그림 1에서는 활동이 회전을 거친 다음 끝날 때까지 활동의 다양한 수명 주기 상태를 보여줍니다. 또한 관련 활동 수명 주기 옆에 ViewModel의 전체 기간도 보여줍니다. 바로 이 다이어그램에서는 활동의 상태를 보여줍니다. 동일한 기본 상태가 프래그먼트의 수명 주기에 적용됩니다.

활동 상태 변경에 따른 ViewModel의 수명 주기를 나타내는 그림

일반적으로 시스템에서 활동 객체의 onCreate() 메서드를 처음 호출할 때 ViewModel을 요청합니다. 시스템은 활동 기간 내내(예: 기기 화면이 회전될 때) onCreate() 메서드를 여러 번 호출할 수 있습니다. ViewModel는 처음 ViewModel를 요청한 시점부터 활동이 끝나고 폐기될 때까지 존재합니다.

ViewModel 종속 항목 삭제

ViewModel은 수명 주기 과정에서 ViewModelStoreOwner에 의해 ViewModel이 소멸될 때 onCleared 메서드를 호출합니다. 이렇게 하면 ViewModel의 수명 주기를 따르는 모든 작업 또는 종속 항목을 정리할 수 있습니다.

다음 예는 viewModelScope의 대안을 보여줍니다. viewModelScope은 ViewModel의 수명 주기를 자동으로 따르는 내장 CoroutineScope입니다. ViewModel은 이를 사용하여 비즈니스 관련 작업을 트리거합니다. 더 쉬운 테스트를 위해 viewModelScope 대신 맞춤 범위를 사용하려면 ViewModel은 CoroutineScope을 생성자의 종속 항목으로 수신하면 됩니다. 수명 주기가 끝날 때 ViewModelStoreOwner가 ViewModel을 삭제하면 ViewModel도 CoroutineScope을 취소합니다.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

수명 주기 버전 2.5 이상부터 ViewModel 인스턴스가 삭제될 때 자동으로 닫히는 Closeable 객체를 한 개 이상 ViewModel 생성자에 전달할 수 있습니다.

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 ...
}

권장사항

다음은 ViewModel을 구현할 때 따라야 할 몇 가지 주요 권장사항입니다.

  • 범위 지정으로 인해 ViewModel은 화면 수준 상태 홀더의 구현 세부정보로 사용합니다. 칩 그룹이나 양식과 같은 재사용 가능한 UI 구성요소의 상태 홀더로 사용하지 마세요. 그러지 않으면 동일한 ViewModelStoreOwner의 동일한 UI 구성요소를 다른 방식으로 사용할 때 동일한 ViewModel 인스턴스가 생성됩니다.
  • ViewModel은 UI 구현 세부정보에 관해 알 수 없습니다. ViewModel API가 노출하는 메서드의 이름과 UI 상태 필드의 이름을 최대한 일반적으로 유지하세요. 그러면 ViewModel이 휴대전화, 폴더블, 태블릿 또는 Chromebook과 같은 모든 유형의 UI를 수용할 수 있습니다.
  • ViewModelStoreOwner보다 오래 지속될 수 있으므로 ViewModel은 메모리 누수를 방지하기 위해 Context 또는 Resources와 같은 수명 주기 관련 API의 참조를 보유해서는 안 됩니다.
  • ViewModel을 다른 클래스, 함수 또는 기타 UI 구성요소에 전달하면 안 됩니다. 플랫폼에서 이를 관리하므로 최대한 가깝게 유지해야 합니다. 활동이나 프래그먼트, 화면 수준의 구성 가능한 함수 가까이 유지합니다. 이렇게 하면 하위 수준 구성요소가 필요한 것보다 더 많은 데이터와 로직에 액세스할 수 없습니다.

추가 정보

데이터가 더 복잡해지면 데이터 로드만을 위한 별도의 클래스를 사용하는 것이 좋습니다. ViewModel의 목적은 UI 컨트롤러의 데이터를 캡슐화하여 구성이 변경되어도 데이터를 유지하는 것입니다. 구성 변경 시 데이터를 로드, 유지 및 관리하는 방법에 관한 자세한 내용은 저장된 UI 상태를 참고하세요.

Android 앱 아키텍처 가이드에서는 이러한 함수를 처리하는 저장소 클래스 빌드를 제안합니다.

추가 리소스

ViewModel 클래스에 관한 자세한 내용은 다음 리소스를 참고하세요.

문서

샘플