AAC ViewModel
안드로이드 앱에서 화면 전환, 화면 크기 변경, 언어설정, 키보드 숨김 변경 등의 경우 configuration change(구성 변경) 이 일어난다.
이때 Activity/Fragment 에서 일시적으로 onDestory() 되었다가 다시 onCreate() 되면서 화면이 다시 그려지게 된다.
그렇기 때문에 어떤 data가 뷰를 통해 보여주고 있었다면 data 가 초기화 되는 현상이 발생할 수 있다.
이를 방지하기 위해서는 화면이 다시 그려져도 data가 보존될 수 있도록 해야 하는데 그 방법도 다양하다.
SharedPreference를 사용해 전역적으로 저장하거나 DB 등을 이용할 수도 있고,
일반적으로는 onSaveInstanceState() 메서드를 사용해 destroy 전 화면 정보를 저장하고 복원한다.
하지만 빠르게 데이터를 복원하고 리스트 데이터나 비트맵 같이 큰 데이터를 저장해야 할때는 적절하지 않다.
이런 문제를 해결하기 위해 나온 것이 바로 AAC의 ViewModel이다.
ViewModel은 액비비티가 명시적으로 finish 될때까지 살아있기 때문에 화면전환이 발생해 onDestroy() 와 onCreate()가 여러번 발생해도 데이터를 유지할 수 있다.
즉, ViewModel 클래스는 라이프 사이클을 파악하여 UI 와 관련된 데이터를 저장하고 관리한다.
AAC의 ViewModel을 만들고 사용하기 위해서는 ViewModelProvider 와 ViewModelFacotry가 중요한 개념이라고 생각한다.
ViewModelFactory
- 실제로 ViewModel을 만들어냄
ViewModelProvider
- ViewModel 인스턴스를 가져와 반환하는 역할
- ViewModel이 이미 존재한다면 찾아서 그렇지 않다면 ViewModelFactory를 통해 생성한 다음 ViewModel을 객체화하여 반환
ViewModelProvider
class ViewModelProvider(owner: ViewModelStoreOwner, factory: Factory) {
constructor(owner: ViewModelStoreOwner) : this(owner, DefaultFactory())
private val viewModelStore: ViewModelStore = owner.viewModelStore
private val factory: Factory = factory
}
ViewModelProvider는 매개변수로 ViewModelStoreOwner 와 Factory 객체를 받는다.
ViewModelStoreOwner는 일반적으로 액티비티나 프래그먼트 같이 수명주기를 갖는 컴포넌트를 이야기 한다.
이 컴포넌트의 하위 컴포넌트인 AppCompatActivity 나 FragmentActivity를 넣어주면 컴포넌트 내부적으로 ViewModelStoreOwner를 반환한다. 그리고 ViewModelStoreOwner는 ViewModelStore() 메서드 1개만 가진 인터페이스로 컴포넌트에 해당하는 Store를 반환한다. Store는 onStop과 onDestroy 사이쯤 시스템에 의해 호출되는 mLastNonConfigurationInstances 메서드에서 액티비티의 일부 변수들을 저장해주는 과정이 있는데 이때 저장된다.
Configuration이 일어나 다시 ViewModel를 생성하려고 하면 viewModelStore에 같은 키값의 뷰모델이 있는 확인하고 있으면 해당 뷰모델을 반환하고 없으면 생성해준다. (기존에 생성되었던 뷰모델을 그대로 사용하기 때문에 데이터도 유지)
ViewModel을 사용하기 위해 ViewModelProvider를 사용해 ViewModelStoreOwner값을 넘겨준다 -> 액티비티 컴포넌트에 대해 ViewModelStore를 가져온다 -> ViewModelStore에 저장된 ViewModel이 있으면 가져오고, 그렇지 않으면 새로 생성한다
ViewModelFactory
class MyViewModelFactory(private val someParameter: String) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(someParameter) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
ViewModelFactory는 ViewModel 인스턴스를 생성하기 위한 팩토리 클래스이다.
기본적으로 ViewModel은 매개변수 없는 생성자를 가져야 하지만 때로는 생성자에 매개변수를 전달하여 특정 설정 또는 종속성을 제공한다. 대표적인 예로 공통적으로 사용하는 Repository 나 DatasourceImp가 있는 경우 이를 전달한다.
액티비티별로 서로 다른 ViewModel을 만들거나 서로 다른 Repository, DatasourceImp를 사용하는 경우 팩토리를 사용해 다형성을 보장 받을 수 있다.
나의 경우 특정 기능별로 ViewModel을 나눠 개발했고 AAC의 Repository 구조를 사용해서 Repository 또한 기능별로 따로 만들었다.
(AAC에 대해 잘 모른채로 개발했던 코드라 AAC 포스팅 후 다시 와서 수정하려고 한다...)
API 호출 함수를 구현한 RemoteDataSourceImp를 공통적으로 사용하고 각 기능별로 만들 Repository에서 RemoteDataSourceImp를 상속받아 필요한 Imp 메서드들만 호출하도록 하는 메서드를 작성했다.
그리고 이렇게 뷰모델별로 각각에 맞는 Repository들을 세팅해주고 반환하도록 해주었다.
class ViewModelFactory(private val remoteDataSource : RemoteDataSourceImp): ViewModelProvider.Factory {
// 액티비티 별로 서로 다른 뷰모델을 만들기 위해서 ViewModelProvider.Factory 를 implements 함
// 뷰모델을 만드는 create() 메서드를 오버라이딩
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(PhotoGuideListViewModel::class.java) -> {
val repository = PhotoGuideRepository(remoteDataSource)
PhotoGuideListViewModel(repository) as T
}
modelClass.isAssignableFrom(PhotoMakerViewModel::class.java) -> {
val repository = PhotoMakerRepository(remoteDataSource)
PhotoMakerViewModel(repository) as T
}
else -> {
throw IllegalArgumentException("Failed to create ViewModel : ${modelClass.name}")
}
}
}
}
val viewModel : PhotoMakerViewModel by viewModels { ViewModelFactory(RemoteDataSourceImp(RetrofitInstance)) }
파라미터를 어떻게 넣느냐에 따라 ViewModelProvider를 사용하는 방법이 다양하고, ViewModel을 선언하는 방법도 다양하다.
ViewModelProvider를 사용하는 3가지 방법
1번. 뷰모델에 생성자가 없는 경우 -> viewmodelprovider에 직접 Factory 안 넣는 경우
viewmodelprovider 매개변수로 컴포넌트만 넣어주며 내부적으로 Default Factory를 사용한다.
noParamViewModel = ViewModelProvider(this)
2번. 뷰모델에 생성자가 없는 경우 -> viewmodelprovider에 Factory 생성해서 넣어주는 경우
noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
3번. 뷰모델에 생성자 파라미터가 있는 경우
뷰모델을 생성할 때 넘겨주는 매개변수가 있는 경우 커스텀 Factory Class 만들고 create() 메서드를 오버라이딩 해서 넣어주어야 한다.
(ViewModelFactory 코드를 참고하면 뷰모델 생성시 repository를 넣어주기 때문에 따로 Factory를 만들어 사용했다.)
hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(repository))
.get(HasParamViewModel::class.java)
ViewModel을 생성하는 2가지 방법
1번. ViewModelProvider를 사용
ViewModelProvider는 클래스를 직접 사용해 ViewModel 인스턴스를 생성하는 방법으로 AndroidX 라이브러리의 lifecycle-viewmodel-ktx 모듈에 포함되어 있다.
/* 뷰모델 생성자 파라미터 없는 경우 */
viewModel = ViewModelProvider(this).get(PhotoGuideDetailViewModel::class.java)
/* 뷰모델 생성자 파라미터 있는 경우 */
//viewModel = ViewModelProvider(this,ViewModelFactory(RemoteDataSourceImp(RetrofitInstance)))
// .get(PhotoGuideDetailViewModel::class.java)
2번. viewmodels 프로퍼티를 사용
viewModels는 AndroidX 라이브러리의 fragment-ktx 또는 activity-ktx 모듈에 포함되어 있는 Kotlin 확장 프로퍼티다.
viewModels는 ViewModel 인스턴스를 생성하고 ViewModelProvider에 의해 관리되는 라이프사이클에 바인딩한다.
val viewModel : PhotoMakerViewModel by viewModels { ViewModelFactory(RemoteDataSourceImp(RetrofitInstance)) }
💡 ViewModel에서 Context 참조?
ViewModel 에서는 Activity/Fragment/View 를 함부로 참조해서는 안 된다.
configuration이 발생해 액티비티나 프래그먼트가 재생성되면 해당 context는 사라지는 것이다. 따라서 ViewModel에서 Context를 함부로 참조했다가는 유효하지 않은 context에 접근해 오류가 나는 Dangling Reference 와 같은 문제를 겪을 수 있다.
하지만 불가피하게 viewmodel에서 context를 사용해야 하는 경우가 있다.
이런 경우 안드로이드에서 제공하는 ViewModelProvider.AndroidViewModelFactory 를 사용하면 된다.
/* 생성자 파라미터 없는 경우 */
class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)
/* 생성자 파라미터 있는 경우 커스텀 팩토리 필요 */
class HasParamAndroidViewModel(application: Application, val repository: PhotoGuideRepository)
: AndroidViewModel(application)
//커스텀 팩토리 매개변수로 context, 원하는 매개변수를 넣어준다
class HasParamAndroidViewModelFactory(private val application: Application, private val remoteDataSource : RemoteDataSourceImp)
: ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
try {
val repository = PhotoGuideRepository(remoteDataSource)
return modelClass.getConstructor(Application::class.java, PhotoGuideRepository::class.java)
.newInstance(application, repository)
} 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)
}
}
💡 AAC의 ViewModel 과 MVVM의 ViewModel은 다르다
이전에 MVVM 관련 포스팅에서 AAC의 ViewModel을 사용한다고 해서 MVVM 패턴이 구현되는 것은 아니라고 거듭 강조했다.
두 ViewModel은 완전히 다른 녀석(?)인데 특히 차이점이 있다면
MVVM ViewModel의 경우 하나의 뷰가 여러개의 뷰모델을 갖을 수 있다.
하나의 뷰에서 여러 도메인 데이터를 다뤄야 하는 경우 (보통 도메인 별로 뷰모델 만드니까..) 하나의 뷰가 여러개의 뷰모델을 가질 수 있는 1:N 관계이다.
하지만 AAC ViewModel은 Activity 내에서 1개만 생성 가능한 싱글톤 패턴의 객체이다. 따라서 Activity 내의 여러 Fragment를 가질시에 여러 Fragment에 각자의 ViewModel을 사용할 수 없습니다. (Fragment들이 Activity에 대해 생성된 하나의 ViewModel을 공유할 수는 있다)
https://black-jin0427.tistory.com/322
https://wooooooak.github.io/android/2020/10/11/AAC_VewModel_internal/
'Android' 카테고리의 다른 글
[안드로이드/AI] 앱에서 Segmentation 모델을 사용하기 위한 다양한 시도 (0) | 2023.08.17 |
---|---|
[안드로이드/아키텍쳐] 안드로이드 공식 권장 아키텍쳐 (구vs신버전) (0) | 2023.07.19 |
[안드로이드/아키텍쳐] MVC, MVVM 패턴 그리고 ViewModel (0) | 2023.07.13 |
[안드로이드/Activity] Parcelable, Serializable 그리고 Parcelize (1) | 2023.07.13 |
[안드로이드/소셜로그인&Oauth] AWS와 통신하는 안드로이드 앱에서 Google Oauth2.0 사용하기 (4) | 2023.07.13 |