이 글은 이전 포스팅([Android] 화면 회전해도 데이터 유지하기 - AAC ViewModel)에 이어지는 글입니다.
ViewModel 클래스를 상속하여 정의한 클래스는 개발자가 직접 생성자를 통하여서 인스턴스를 생성할 수 없고, ViewModelProvider.Factory 인터페이스를 필요로 합니다.
이번 포스팅을 통하여서 안드로이드는 어떻게 ViewModel 을 관리하고, 생성하는지 알아보고 필요에 따라 어떤 방식으로 뷰모델 객체를 생성해야 하는지 살펴보도록 하겠습니다.
ViewModelStoreOwner / ViewModelStore
ViewModel 은 ViewModelStore 라는 객체에서 관리를 합니다.
ViewModelStore 클래스는 내부적으로 HashMap<String, ViewModel> 를 두어 ViewModel 을 관리합니다.
그러면 이 ViewModelStore 객체는 누가 어떻게 만들고 관리할까요?
그건 바로 ViewModelStoreOwner 라는 녀석이 합니다.
ViewModelStoreOwner 는 다음과 같이 생긴 인터페이스이고, FragmentActivity 의 부모격인 ComponentActivity와 Fragment 클래스가 이를 구현(Implement) 하고 있습니다.
// Java Code
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
액티비티와 프래그먼트가 바로 이 ViewModelStoreOwner 를 구현하고 있기 때문에, 우리는 ViewModel 객체를 생성할 때 액티비티나 프래그먼트를 필요로 하고, 어떤 Owner 를 통해 생성하냐에 따라 ViewModel 의 Scope 가 결정됩니다.
그렇다면 액티비티나 프래그먼트만 있으면 ViewModel 인스턴스를 생성할 수 있을까요?
아니겠죠. 앞서 말씀 드린것처럼 ViewModel 을 생성할 때는 실질적으로 ViewModel 인스턴스를 생성하는 역할을 하는 팩토리를 필요로 합니다. (팩토리 패턴이 낯선 분들은 여기를 참고하세요)
ViewModelProvider 안에 정의되어 있는 팩토리는 다음과 같이 정의되어 있습니다.
// Java Code
public class ViewModelProvider {
public interface Factory {
@NonNull
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}
}
이제 ViewModel 인스턴스를 생성하기 위한 재료들은 전부 확인 했으니, 실습을 통해 직접 생성해보도록 하겠습니다.
ViewModelProviders -> Deprecated
정상적인 실습을 하기에 앞서, 많은 블로그들과 심지어 구글 공식 Developer 사이트에서도 아직까지 예제로 사용되고 있는 ViewModelProviders 에 대해 짚고 넘어가겠습니다.
ViewModelProviders.of(this).get(AnyViewModel::class.java) --> deprecated !!
ViewModelProviders 클래스는 Deprecated 되었습니다. "Deprecated 되었다" 라는 말은 해당 클래스(또는 메소드)를 더이상 개발하지 않는 것을 뜻하며 특정 버전 이후로 호환되지 않을 수 있기 때문에 더이상 사용하지 않아야한다는 것을 의미합니다.
대체로 deprecated 되는 기능들에 대해서는 대안이 함께 제시되는데요. 이 경우에는 ViewModelProviders 대신에 ViewModelProvider 를 사용하도록 권장하고 있습니다. 따라서 우리는 ViewModelProvider 를 통해 ViewModel 객체를 생성하는 방법에 대해서만 다루도록 하겠습니다.
1. 파라미터가 없는 ViewModel - Lifecycle Extensions
이 방법은 가장 편한 방법 중 하나입니다. androidx.lifecycle의 lifecycle-extensions 모듈을 가져와 사용하면 됩니다.
먼저, module 수준의 build.gradle 에 다음과 같이 디펜던시를 추가해줍니다.
dependencies {
// ...
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}
다음으로 예제에 사용할 ViewModel 클래스를 정의하겠습니다. 파라미터와 멤버 변수, 함수를 갖고있지 않은 심플한 ViewModel 클래스입니다.
// 파라미터가 없는 ViewModel class
class NoParamViewModel : ViewModel()
이제 이 뷰모델 클래스를 통하여 액티비티에서 객체를 생성해주겠습니다.
class MainActivity : AppCompatActivity() {
private lateinit var noParamViewModel: NoParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* use ViewModelProvider's constructor provided from lifecycle-extensions package */
noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
}
}
보시다시피 ViewModelProvider 생성자에 this 를 포함해주고, get() 메소드 내에 생성하고자 하는 뷰모델 클래스 타입을 넣어주면 됩니다.
이때 this 는 ViewModelStoreOwner 타입이기 때문에 액티비티나 프래그먼트를 넣어주시면 됩니다.
2. 파라미터가 없는 ViewModel - ViewModelProvider.NewInstanceFactory
이번에 살펴볼 방법은 NewInstanceFactory 입니다.
이는 안드로이드가 기본적으로 제공해주는 팩토리 클래스이며, ViewModelProvider.Factory 인터페이스를 구현하고 있습니다. 따라서 ViewModel 클래스가 파라미터를 필요로 하지 않거나, 특별히 팩토리를 커스텀 할 필요가 없는 상황에서는 1번 방법을 사용하거나, 2번 방법을 사용하면 되겠습니다.
2번 방법은 1번에서 추가해준 lifecycle-extenstions 모듈을 추가하지 않아도 사용가능한 방법입니다.
class MainActivity : AppCompatActivity() {
private lateinit var noParamViewModel: NoParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
.get(NoParamViewModel::class.java)
}
}
3. 파라미터가 없는 ViewModel - ViewModelProvider.Factory
이번에는 직접 ViewModelProvider.Factory 인터페이스를 구현하여 보도록 하겠습니다.
이 방법의 장점은 하나의 팩토리로 다양한 ViewModel 클래스를 관리할 수도 있고, 원치 않는 상황에 대해서 컨트롤 할 수 있습니다.
class NoParamViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(NoParamViewModel::class.java)) {
NoParamViewModel() as T
} else {
throw IllegalArgumentException()
}
}
}
위 코드는 NoParamViewModel 클래스가 아니면 IllegalArgumentException 을 던지도록 구현되어 있습니다. 이는 어디까지나 개발자의 마음대로 구현하면 되는 부분이며, 어떤 타입의 클래스가 전달되더라도 인스턴스를 생성하도록 구현할 수도 있습니다.
4. 파라미터가 있는 ViewModel - ViewModelProvider.Factory
3번 방법의 연장선상에서, ViewModelProvider.Factory 를 구현하면 파라미터를 소유하고 있는 ViewModel 객체의 인스턴스를 생성할 수 있습니다. 직접 구현한 Factory 클래스에 파라미터를 넘겨주어 create() 내에서 인스턴스를 생성할 때 활용하면 됩니다.
그럼 이번에는 파라미터가 있는 ViewModel 을 정의하고 그에 대한 객체를 생성하는 예제를 작성해보겠습니다.
ViewModel
class HasParamViewModel(val param: String) : ViewModel()
ViewModelFactory
class HasParamViewModelFactory(private val param: String) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(HasParamViewModel::class.java)) {
HasParamViewModel(param) as T
} else {
throw IllegalArgumentException()
}
}
}
Activity (or Fragment)
class MainActivity : AppCompatActivity() {
private lateinit var hasParamViewModel: HasParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val sampleParam = "Ready Story"
hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(sampleParam))
.get(HasParamViewModel::class.java)
}
}
3번과 4번 예제에서는 각각 파라미터 유무에 따라 별도의 팩토리 클래스를 구현하였지만, 꼭 그럴 필요 없이 하나의 팩토리 클래스로 두 가지 상황에 대한 처리를 한꺼번에 할 수도 있습니다.
5. 파라미터가 없는 AndroidViewModel - AndroidViewModelFactory
이번에는 조금 다른 ViewModel 을 살펴보겠습니다.
사실 developer 사이트에 의하면 ViewModel 클래스에서 Context 객체를 소유하거나 접근하는 것에 있어서 권장하지 않고 있습니다. 하지만 정말 불가피하게 필요한 경우가 있을 수 있는데요. ViewModel 에서 Context 를 사용해야할 필요성이 있을 때는 AndroidViewModel 클래스를 사용하면 됩니다.
그리고 안드로이드에서는 이러한 AndroidViewModel 객체에 대한 생성을 위해 ViewModelProvider.AndroidViewModelFactory 라는 별도의 팩토리를 제공합니다. 예제를 통해 살펴보겠습니다.
먼저 AndroidViewModel 을 상속하는 ViewModel 클래스를 정의해줍니다.
class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)
보시다시피 AndroidViewModel 은 Application 객체를 필요로 합니다.
이번에는 AndroidViewModelFactory 를 이용하여 뷰모델 객체를 생성해보겠습니다.
class MainActivity : AppCompatActivity() {
private lateinit var noParamAndroidViewModel: NoParamAndroidViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noParamAndroidViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))
.get(NoParamAndroidViewModel::class.java)
}
}
AndroidViewModelFactory 내부 코드를 살펴보면 2번에서 살펴본 NewInstanceFactory 를 상속한 코드라는 걸 확인할 수 있습니다.
6. 파라미터가 있는 AndroidViewModel
드디어 마지막입니다. 파라미터가 있는 AndroidViewModel 객체를 생성하는 방법인데요.
사실 4번의 방법으로도 가능합니다만, 이번에는 5번에서 살펴본 AndroidViewModelFactory 와 유사한 방식으로 커스텀 팩토리를 구현해보도록 하겠습니다.
먼저 파라미터가 있는 AndroidViewModel 클래스를 준비합니다.
class HasParamAndroidViewModel(application: Application, val param: String)
: AndroidViewModel(application)
다음으로 Custom Factory를 구현해줍니다.
class HasParamAndroidViewModelFactory(private val application: Application, private val param: String)
: ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
try {
return modelClass.getConstructor(Application::class.java, String::class.java)
.newInstance(application, param)
} catch (e: NoSuchMethodException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
} catch (e: IllegalAccessException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
} catch (e: InstantiationException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
} catch (e: InvocationTargetException) {
throw RuntimeException("Cannot create an instance of $modelClass", e)
}
}
return super.create(modelClass)
}
}
제법 복잡해보이지만, catch 문을 많이 분기했을 뿐이지 실상은 복잡하지 않습니다.
위 코드에서는 ViewModelProvider.NewInstanceFactory 클래스를 상속하여 구현했지만, ViewModelProvider.Factory 인터페이스를 구현하여도 무방합니다.
긴 글이었습니다. 어떤 방법으로든 각자의 상황에 맞게 사용하시면 되고, 이번 포스팅에서 다루지는 않았지만 android-ktx / fragment-ktx 모듈을 사용하면 보다 편리하게 뷰모델 인스턴스를 생성할 수도 있습니다.
위에서 작성된 모든 예제 코드는 Github 저장소에서 확인하실 수 있습니다.
'Android > Jetpack' 카테고리의 다른 글
[Android] DataBinding + StateFlow + Sealed class 예제 (11) | 2021.09.10 |
---|---|
[Android] LiveData VS StateFlow, 왜 StateFlow 를 써야할까? (8) | 2021.08.26 |
[Android] LiveData 유연하게 사용하기 - Transformations.map, switchMap (4) | 2020.04.21 |
[Android] 화면 회전해도 데이터 유지하기 - AAC ViewModel (1) | 2020.04.15 |
[Android] AAC ViewModel과 찰떡 궁합! LiveData 이해하기 (1) | 2019.10.07 |