현재 구글은 동일한 액티비티 안에서 다른 화면을 콘텐츠로 로드하는 단일 액티비티 앱을 권고하고 있다.
모던 아키텍처 가이드라인 또한 앱의 다양한 책임 부분을 완전히 별도의 모듈로 나누는 것을 권장한다. 이 접근 방식의 핵심 중 하나가 ViewModel 컴포넌트다.
ViewModel의 목적은 사용자 인터페이스와 관련된 데이터 모델과 앱의 로직을 사용자 인터페이스를 표시 및 관리하고 운영체제와 상호작용하는 코드와 분리하는 것이다.
이런 방식으로 디자인된 앱은 하나 이상의 UI 컨트롤러(액티비티 등)와 이 컨트롤러들이 필요로 하는 데이터를 처리하는 ViewModel 인스턴스로 구성된다.
ViewModel은 별도의 클래스로 구현되며, 모델 데이터와 그 데이터를 관리하기 위해 호출될 수 있는 함수들을 포함한 상탯값을 포함한다. 버튼 클릭 같은 모델 데이터와 관련된 사용자 인터페이스 이벤트들은 ViewModel 안에서 적절한 함수를 호출하도록 설정되어 있다.
앱의 라이프사이클과 동안 하나의 액티비티가 얼마나 많이 재생성되는가에 관계없이 ViewModel 인스턴스는 메모리에 남아 있기 때문에 데이터 일관성을 유지한다. 예를 들어, 단일 액티비티 앱에서 한 액티비티가 이용하는 하나의 ViewModel은 앱이 종료될 때가 아니라 액티비티가 완료될 때까지 메모리에 남아 있는다.
viewModel은 액티비티의 사용자 인터페이스에서 관찰할 수 있는 데이터를 저장하는 것을 주요 목표로 한다.
컴포저블 안에서 선언되는 상태와 비슷하게 viewModel 상태는 함수들의 mutableStateOf 그룹을 이용해 선언한다. 다음 viewModel 선언에서는 하나의 정수 카운트값을 포함하는 상태를 선언한다.
class MyViewModel: ViewModel() {
var customerCount by mutableStateOf(0)
}
위의 코드에서는 모델 안에 일부 데이터를 캡슐화했다. 다음으로 UI 안에서 호출되어 카운터값을 변경할 수 있는 함수를 추가한다.
class MyViewModel: ViewModel() {
var customerCount by mutableStateOf(0)
fun increaseCount() {
customerCount++
}
}
아무리 복잡한 모델이라 할지라도 본질적으로는 이 두 가지 기본 상태와 함수 빌딩 블록을 조합해서 사용한 것에 지나지 않는다.
뷰모델은 사용자 인터페이스를 구성한 컴포저블 안에서 이용해야만 쓸모가 있다. 이를 위해서는 뷰모델 인스턴스를 컴포저블에 파라미터로 전달해, 컴포저블에서 상탯값과 함수에 접근할 수 있도록 해야한다. 이는 컴포저블 계층의 맨 위에 위치한 컴포저블에서 수행할 것을 권장한다(이후 필요에 따라 모델 상태와 이벤트 핸들러를 자식 컴포저블로 전달할 수 있다) 다음은 액티비티 안에서 뷰모델에 접근할 수 있도록 한 예시이다.
class MyViewModel: ViewModel() {
var customerCount by mutableStateOf(0)
fun increaseCount() {
customerCount++
}
}
class IntroductionViewModelActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TopLevel()
}
}
}
}
}
@Composable
fun TopLevel(model: MyViewModel = MyViewModel()) {
VMMainScreen(model.customerCount) {model.increaseCount()}
}
@Composable
fun VMMainScreen(count: Int, addCount: () -> Unit = {}) {
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()) {
Text("Total customers = $count",
Modifier.padding(10.dp))
Button(
onClick = addCount
) {
Text(text = "Add a Customer")
}
}
}
이 예시에서 첫 번째 함수 호출은 onCreate() 메서드에서 TopLevel 컴포저블로 이루어진다. TopLevel 컴포저블은 viewModel() 함수 호출을 통해 초기화된 기본 viewModel 파라미터를 이용해 선언된다.
TopLevel 함수는 뷰모델 인스턴스에 접근할 수 있으므로 뷰모델의 customerCount 상태 변수와 increaseCount() 함수에 대한 참조에 접근할 수 있다. 함수는 이들을 VMMainScreen 컴포저블에 전달한다.
위의 코드를 실행하고 Button을 클릭하면 뷰 모델의 increaseCount() 함수를 호출하게 되고 customerCount 값이 증가한다. 이는 사용자 인터페이스 재구성으로 이어지며 Text 컴포저블에 새로운 값이 표시된다.
LiveData는 뷰모델 안에서 데이터값을 감싸기 위한 목적으로 이용할 수 있다.
LiveData 인스턴스들은 MutableLiveData 클래스를 이용해 뮤터블로 선언할 수 있으며, 뷰모델 함수를 이용해 감싸진 데이터값을 변경할 수 있다. 예를 들어, 다음은 상태 대신 MutableLiveData를 이용해 고객 이름을 저장하도록 디자인한 모델의 코드 예시다.
class MyViewModel: ViewModel() {
var customerName: MutableLiveData<String> = MutableLiveData("")
fun setName(name: String) {
customerName.value = name
}
}
새로운 값은 value 프로퍼티를 이용해 라이브 데이터 변수에 할당해야 한다는 점에 주의한다.
state와 마찬가지로 LiveData를 다룰 때는 가장 먼저 컴포저블을 초기화하는 과정에서 뷰 모델의 인스턴스를 얻어야 한다.
@Composable
fun TopLevel(model: MyViewModel = MyViewModel()) {
val customerName: String by model.customerName.observeAsState("")
}
위의 코드에서 observeAsState()를 호출하면 라이브 데이터값이 상태 인스턴스로 바뀌고, 인스턴스는 customerName 변수에 할당된다. 변환된 후 해당 상태는 저장된 값이 변경될 때마다 재구성을 트리거하는 것을 포함해 다른 상태 객체와 동일하게 동작한다.