안드로이드에서 JetPack은 기존에 구글에서 제공하던 서포트 라이브러리와 아키텍처 컴포넌트를 모아 놓은 것입니다. JetPack은 완전히 새로운 기법이라기보다는 안드로이드 앱 개발에 도움을 주고자 기존의 라이브러리를 통한한 개념입니다.
안드로이드 앱을 개발하다 보면 코드가 복잡해지기 마련인데, 이때 특정한 패턴이나 소프트웨어 모델을 적용해 개발하면 깔끔하게 구조화할 수 있습니다. 그런데 안드로이드에서 지원이 미약하다보니 개발자마다 각기 다른 모델을 선택할 수밖에 없었습니다. 어떤 개발자는 MVVM(Model - View - ViewModel)을 적용하고, 어떤 개발자는 MVP(Model - View - Presenter)를 적용하기도 했습니다.
이러한 문제를 해결하고자 나온 것이 AAC(Android Archietecture Components)입니다. AAC에서 앱을 구조화하는 여러 가지 기법을 제공하고 그것을 이용하는 개념입니다.
이전에 작성한 글인 데이터 바인딩 또한 AAC 중 하나이고, AAC에 대한 자세한 내용은 링크에 있습니다.
AAC의 핵심 구조는 MVVM 패턴입니다.
패턴에 대한 정리는 링크에 있습니다.
MVVM(Model - View - ViewModel)은 오래 전부터 있던 패턴입니다. MVVM에서 모델은 업무 로직 처리(데이터 처리 등)를 담당하며, 뷰는 화면을 담당합니다. 업무 처리와 화면을 분리해 개발하자는 개념인데, 이 모델과 뷰를 직접 연결하지 않고 중간에 뷰모델을 두어 뷰에서 뷰모델에 일을 시키면 뷰모델은 모델을 이용하는 구조입니다.
안드로이드에서 뷰는 액티비티 혹은 프레그먼트로 작성합니다. 그리고 이곳에서 필요한 업무 처리를 뷰모델에게 의뢰합니다. 그러면 뷰모델은 실제로 업무처리를 담당하는 모델(데이터베이스 또는 서버 연동 등)을 이용하게 됩니다.
뷰모델은 개발자가 직접 작성할 수도 있는데 AAC에서 똑같은 이름으로 뷰모델 클래스를 제공합니다. 우선 뷰모델을 이용하기 위해서 모듈 수준의 build.gradle 파일에 의존성을 설정합니다.
// 작성일자 기준 최신 버전
def lifecycle_version = "2.4.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
뷰모델은 ViewModel 클래스를 상속받아 작성합니다.
class MyViewModel : ViewModel() {
private val _users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
val users: LiveData<List<User>> = _users
fun getUsers(userLise: List<User>>) {
_users.value = userLise
}
// Model쪽에 List<User> 값을 요청하는 메소드
fun loadUsers() {
// 유저를 불러오는 기능...
}
}
우선 ViewModel을 상속받는 클래스를 작성합니다. 그 안에는 액티비티나 프래그먼트의 UI에 출력할 데이터를 처리하는 코드를 작성합니다. 위의 예제에서는 MutableLiveData 타입의 _users 프로퍼티를 선언하고 users라는 LiveData에서 이 값을 받습니다. MutablaLiveData와 LiveData에 대한 내용은 LiveData에서 설명하겠습니다. 그러면 액티비티나 프래그먼트에서는 users 프로퍼티의 값을 사용해 사용자에게 보여주기만 하면 됩니다.
핵심은 액티비티나 프래그먼트에서는 데이터를 획득해 UI에 보여주기만 하는 것이고 ViewModel안에서 그 데이터를 처리하고 가공하는 작업을 하는 것입니다.
이렇게 선언된 ViewModel은 액티비티에서 아래와 같이 사용됩니다.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 시스템이 액티비티의 onCreate를 처음 호출할 때 뷰모델 객체는 생성
// re-created되는 액티비티는 처음 액티비티가 만들어질 때 생성된 뷰모델 객체를 다시 받습니다
val model: MyViewModel = ViewModelProvider(this@MyActivity).get(MyViewModel::class.java)
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
}
}
위의 코드는 ViewModelProvider의 get 함수를 사용하여 ViewModel 객체를 생성합니다. 그 후 ViewModel 클래스 안에 선언한 getUsers를 호출하며 결과를 받기 위해 observe()의 두 번째 매개변수에 콜백 함수를 람다 함수로 지정하였습니다. getUser() 함수에서 결과가 전달되는 순간 콜백 함수로 지정한 람다가 호출됩니다. 그 람다코드 안에는 UI를 업데이트하는 작업을 진행하면 됩니다. (이 또한 LiveData와 관련된 코드로 LiveData와 관련된 글에서 설명하겠습니다.)
위의 코드에서 ViewModelProvider
를 사용하여 ViewModel
객체를 생성하였습니다. ViewModelProvider는 Scope에 ViewModel들을 제공하는 유틸리티 클래스입니다. 액티비티나 프래그먼트를 위한 기본적인 ViewModelProvider는 액티비티나 프래그먼트를 생성자에 전달하면 획득할 수 있습니다.(ViewModelProvider(MyActivity)
와 같이)
위의 사진에서 첫 번째 생성자를 사용하여 ViewModelProvider를 생성한 것입니다. 여기서 액티비티나 프래그먼트를 전달한다고 하였는데 왜 ViewModelStoreOwner를 전달하는 첫 번째 생성자에 해당하는 것인지 의문이 들 수 있습니다.
ViewModelStoreOwner는 인터페이스로 AppCompatActivity, ComponentActivity, Fragment등이 이를 구현하고 있습니다. 그래서 액티비티나 프래그먼트를 ViewModelProvider의 생성자로 넘길 수 있는 것입니다.
ViewModelStoreOwner는 ViewModelStore
를 관리하는 scope입니다. 이 인터페이스를 구현하면 구성 변경 중에 소유된 ViewModelStore를 유지하고 scope가 destroy될 때 ViewModelStore.clear()
를 호출하는 책임을 가지게 됩니다. 즉, 액티비티나 프래그먼트가 이것을 구현하고 있어서 이러한 책임을 가지게 되는 것입니다.
ViewModelStoreOwner 인터페이스에 선언된 유일한 메소드로 ViewModelStore를 반환하고 있습니다. 이 메소드를 ComponentActivity(AppCompatActivity - FragmentActivity - ComponentActivity)와 Fragment는 구현하고 있고 그 메소드안에서 ViewModelStore 객체를 반환합니다. 대체 그러면 ViewModelStore는 무엇일까요..?
public class ViewModelStore {
// HashMap 선언(Key는 String, Value는 ViewModel)
private final HashMap<String, ViewModel> mMap = new HashMap<>();
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
final ViewModel get(String key) {
return mMap.get(key);
}
Set<String> keys() {
return new HashSet<>(mMap.keySet());
}
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}
ViewModelStore
의 코드입니다. ViewModelStore는 HashMap(Key는 String, Value는 ViewModel)구조로 ViewModel을 관리하는 클래스입니다. ViewModelStore는 구성 요소의 변경 속에서도 유지되는 인스턴스입니다. 만약 ViewModelStore의 owner(액티비티나 프래그먼트 등)가 구성 요소의 변화(화면 전환 등)로 destroy되고 recreate되더라도, owner의 새로운 객체는 여전히 같은 ViewModelStore를 가지고 있습니다. 그렇기에 아래의 뷰모델 생명주기에서 보겠지만 onwer의 생명주기가 완전히 destroy되지 않는 이상 똑같은 ViewModelStore를 가지고 있고, 이 ViewModelStore가 ViewModel을 관리하기에 데이터가 사라지거나 하지 않는 것입니다.
정리하자면 ViewModelProvider
생성자의 인자로 ViewModelStoreOwner(액티비티나 프래그먼트가 이를 구현하고 있음)
를 주어 ViewModelProvider를 생성합니다. 그리고 ViewModelProvider.get() 메소드를 사용해서 존재하는 ViewModel
을 획득하거나 새로운 ViewModel
을 만듭니다. 최종적으로 이러한 ViewModel은 ViewModelStore
가 관리하는 구조인 것입니다.
ViewModel 클래스를 직접 생성하지 않고 ViewModelProviders(this).get()을 사용하여 생성하는 것은 뷰모델의 생명주기와 관련된 도움을 받을 수 있기 때문입니다. 이 코드로 생성된 뷰모델 객체는 액티비티 스코프 내에서 싱글턴으로 유지됩니다. 하나의 액티비티내에서 하나의 뷰모델을 여러 번 생성하더라도 한 번만 생성됩니다. 물론, 액티비티 객체가 여러 개라면 하나의 뷰모델 클래스더라도 객체는 여러 개 생성됩니다.
액티비티에서 뷰모델을 생성한 후 액티비티의 생명주기 변화와 ViewModel 생명주기의 변화를 보여주는 그림입니다. 위의 그림에서 볼 수 있듯이 액티비티가 화면의 회전으로 인해 소멸되었다가 다시 생성되어도 뷰모델은 소멸되지 않고 그대로 유지됩니다. 이러한 이유로 데이터는 뷰모델에서 관리하고 액티비티는 뷰모델이 전달하는 데이터를 화면에 출력하는 역할만 수행하면 됩니다. 프래그먼트 또한 액티비티와 마찬가지(액티비티의 생명주기 부분에 프래그먼트만 대입하면 됨)입니다.
생성된 뷰모델 객체가 종료되는 것은 액티비티가 종료되거나(finish and destroy) 프래그먼트가 분리되었을(detach) 때 입니다. 이때, 뷰모델은 유일한 생명주기 함수인 onCleared()가 호출되고 이 함수가 호출되었다는 것은 뷰모델 객체가 소멸한다는 의미입니다.
뷰모델 클래스를 만들 때 ViewModel 클래스말고 AndroidViewModel 클래스를 상속받아 작성할 수도 있습니다.
AndroidViewModel 클래스는 ViewModel의 서브 클래스이고 둘의 차이점은 아래와 같습니다.
ViewModel 생성자 | AndroidViewModel 생성자 |
---|---|
만약 ViewModel에서 시스템 서비스를 찾는데 필요한 Context가 필요하다면 AndroidViewModel 클래스를 사용하면 됩니다. AndroidViewModel 클래스의 생성자는 Application 객체가 매개변수로 지정되어 있어 객체 생성 시 자동으로 Application 객체가 전달됩니다. Application 클래스는 Context를 상속받기에 이 객체를 이용해 Context 객체를 쉽게 이용할 수 있습니다.
만약 AndroidViewModel을 사용할 때와 같이 ViewModel 클래스 생성자에 매개변수를 선언하고 생성하려하면 위에서 사용했던 ViewModelProvider의 기본 생성자(ViewModelStoreOwner를 넘겨주는 것)를 사용해서 ViewModel을 생성할 수 없습니다. 기본 생성자를 사용하면 ViewModelProvider는 default factory
를 사용하여 NewInstanceFactory
가 사용되는데 아래 코드를 보며 추가로 설명하겠습니다.
public static class NewInstanceFactory implements Factory {
@SuppressWarnings("ClassNewInstance")
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection TryWithIdenticalCatches
try {
return modelClass.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
}
}
}
위의 코드는 default에 해당하는 NewInstanceFactory
입니다. newInstanceFactory는 Factory(ViewModelProvider안에 선언되어 있는 인터페이스)를 구현하고 있는데
Factory 인터페이스에는 create() 단 하나의 메소드만 존재합니다. 이 메소드는 ViewModel 클래스를 상속한 클래스를 인자로 받아서 객체로 만들고 반환하는 메소드입니다.
NewInstanceFactory의 create 메소드는 T.newInstance()를 반환합니다. 이 메소드에 생성자에 매개변수가 존재하는 ViewModel 클래스가 넘어오면 오류가 발생합니다. 따라서 생성자에 매개변수가 존재하는 ViewModel 클래스를 ViewModelProvider(ViewModelStoreOwner)를 통해 생성하지 못하는 것입니다.
이와 같은 상황에서는 ViewModelProvider.Factory를 사용하면 생성자에 매개변수가 존재하는 ViewModel을 생성할 수 있습니다.
ViewModelProvider.Factory는 ViewModelProvider안에 선언된 인터페이스로 이 인터페이스를 구현하면 ViewModel들을 인스턴화화하는 책임을 가지게 됩니다. 즉, ViewModel 인스턴스를 생성하기 위한 자신만의 구현을 가질 수 있다는 뜻입니다.
// ViewModelProvider.Factory 인터페이스를 구현하는 클래스
// ViewModel 생성자의 인자를 ViewModelProvider.Factory로 전달
class SampleViewModelFactory(
private val app: Application
): ViewModelProvider.Factory {
// create 메소드는 ViewModel의 인스턴스를 만드는 역할
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// modelClass.getConstructor는 Application 형식을 가진 생성자를 가져오고
// newInstance 메소드를 호출하여 ViewModel 인스턴스를 만들고
// 생성자 값을 이 메소드에 전달
try {
return modelClass.getConstructor(Application::class.java)
.newInstance(app)
}
// 예외코드 작성...
}
}
ViewModelProvider.Factory 인터페이스를 구현하는 클래스를 선언하고 ViewModel 생성자의 매개변수에 전달할 인자를 ViewModelProvider.Factory 생성자의 매개변수로 전달합니다.
ViewModelProvider.Factory에 선언된 유일한 메소드인 create 메소드를 오버라이드하는데 이 메소드는 ViewModel 객체를 만드는 역할을 수행합니다.
getConstructor에 생성자의 형식(위의 예제에서는 AndroidViewModel을 사용했다는 가정하에 Application을 셋팅)을 지정하면 그에 맞는 생성자를 가져오고 newInstance에 전달합니다. 그리고 newInstance는 ViewModel의 인스턴스를 만듭니다.
이와 같이 선언하고 액티비티나 프래그먼트에서 ViewModel을 생성하려면 아래와 같이 선언합니다.
val simpleViewModel = ViewModelProvider(this,
SampleViewModelFactory(application)).get(SimpleViewModel::class.java)
Android KTX는 안드로이드 Jetpack과 여러 안드로이드 라이브러리에 포함된 코틀린의 확장 기능입니다. 즉, 코틀린의 확장 함수, 확장 프로퍼티, 람다 등을 안드로이드 라이브러리에 적용하는 것입니다. 이러한 Android KTX 중에 Activity KTX에 포함된 viewModels 메소드를 사용하면 ViewModelProvider를 사용하지 않고 ViewModel을 생성할 수 있습니다.
Anroid KTX에는 Activity KTX 외에 Fragment KTX, Collection KTX, Room KTX 등 많은 모듈이 존재합니다. 모듈을 사용하려면 build.gradle 파일에 의존성을 추가해야 합니다. 현재는 Activity KTX만 사용하므로 아래의 코드를 build.gradle 파일에 추가하였습니다.
// Activity KTX
implementation "androidx.activity:activity-ktx:1.4.0"
private val viewModel:
SimpleViewModel by viewModels { SimpleViewModelFactory(application) }
코틀린의 위임에 해당하는 by 키워드와 viewModels라는 메소드를 사용해 ViewModel을 지연 생성하는 것입니다. 매개 변수는 널이 될 수 있는 함수 타입이 있고 그 함수 타입의 반환값은 ViewModelProvider.Factory입니다. 예제 코드에서는 ViewModelProvider.Factory를 구현한 클래스의 객체를 전달하였습니다. 위와 같이 전달하고 실제로 viewModel이 사용될 때 ViewModel 객체가 생성됩니다.
프래그먼트에는 activityViewModels라는 메소드가 존재합니다. 이는 프래그먼트의 부모 액티비티의 ViewModel에 접근하는 지연 생성입니다. 위의 코드에서 by viewModels를 by activityViewModels로 변경하면 됩니다.
ViewModel을 사용하는 목적은 UI와 관련된 데이터 모델과 모델을 처리하는 로직을 View의(액티비티, 프래그먼트) 코드에서 분리시키자는 것입니다.
ViewModel은 View의 생명주기에 따른 문제점을 해결합니다. 앱의 프로세스동안 화면전환 등으로 인해서 View가 재생성되더라도 ViewModel 메모리에 남아있습니다. 그 이유는 위에서 설명했듯이 ViewModel의 생명주기는 View가 완전히 onDestroy 되어야만 onCleared를 통해서 메모리에서 삭제하기에, 삭제되기 전까지는 언제나 일관성있는 데이터를 제공합니다.
위의 이유로 ViewModel을 안드로이드에서는 Lifecycle-aware components로 분류하고 있습니다. 즉, 생명주기를 인식하고 있는 컴포넌트라는 뜻이고, View의 생명주기를 알고 있기에 위와 같이 분류한 것 같습니다.
참조
깡쌤의 안드로이드 프로그래밍
안드로이드 developer - ViewModel
ViewModelProvider.Factory
AAC ViewModel을 생성하는 6가지 방법
틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!