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,
)
}
}
}
자바
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
의 전체 기간도 보여줍니다. 바로 이 다이어그램에서는 활동의 상태를
보여줍니다. 동일한 기본 상태가 프래그먼트의 수명 주기에 적용됩니다.
일반적으로 시스템에서 활동 객체의 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 구성요소의 상태 홀더로 사용하지 마세요. 그렇지 않으면 동일한 UI 구성요소를 서로 다른 방식으로 사용하는 ViewModel 인스턴스 칩당 명시적 뷰 모델 키를 사용하지 않는 한 ViewModelStoreOwner의 클래스로 설정해야 합니다.
- ViewModel은 UI 구현 세부정보에 관해 알 수 없습니다. ViewModel API가 노출하는 메서드의 이름과 UI 상태 필드의 이름을 최대한 일반적으로 유지하세요. 그러면 ViewModel이 휴대전화, 폴더블, 태블릿 또는 Chromebook과 같은 모든 유형의 UI를 수용할 수 있습니다.
- ViewModel은
ViewModelStoreOwner
보다 오래 지속될 수 있으므로 수명 주기 관련 API의 참조(예:Context
)를 보유해서는 안 됩니다. 또는Resources
를 호출하여 메모리 누수를 방지합니다. - ViewModel을 다른 클래스, 함수 또는 기타 UI 구성요소에 전달하면 안 됩니다. 플랫폼에서 이를 관리하므로 최대한 가깝게 유지해야 합니다. 활동이나 프래그먼트, 화면 수준의 구성 가능한 함수 가까이 유지합니다. 이렇게 하면 하위 수준 구성요소가 필요한 것보다 더 많은 데이터와 로직에 액세스할 수 없습니다.
추가 정보
데이터가 더 복잡해지면 데이터 로드만을 위한 별도의 클래스를
사용하는 것이 좋습니다. ViewModel
의 목적은 UI 컨트롤러의 데이터를 캡슐화하여 구성이 변경되어도 데이터를 유지하는 것입니다. 구성 변경 시 데이터를 로드, 유지 및 관리하는 방법에 관한 자세한 내용은 저장된 UI 상태를 참고하세요.
Android 앱 아키텍처 가이드에서는 이러한 함수를 처리하는 저장소 클래스 빌드를 제안합니다.
추가 리소스
ViewModel
클래스에 관한 자세한 내용은 다음 리소스를 참고하세요.
문서
샘플
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 수명 주기 인식 구성요소로 Kotlin 코루틴 사용
- UI 상태 저장
- 페이징 데이터 로드 및 표시