*개인적으로 분석한 내용이므로 틀린 내용이 있을 수 있습니다. 틀린 내용을 댓글로 남겨주시면 수정하도록 하겠습니다!
[1]. ViewModel 의 개념
1. 안드로이드 Activity 또는 Fragment 는 각자의 독립된 생명주기를 가지고 있다. 예를들어 B Activity → B Activity 로 전환된다면 A Activity 는 onStop callback 메서드가 실행되며 B Activity 는 onCreate callback 메서드가 호출되게 되어 화면에는 b Activity 가 출력되게 된다. 만약 사용자가 스마트폰을 가로에서 세로로 또는 세로에서 가로로 전환하는 경우등의 기기 구성 변경이 발생하게 되면 안드로이드 시스템은 변경된 기기 구성에 맞춰서 기존의 Activity 는 파괴하고 Activity 를 다시 생성하게 된다. 만약 앱을 개발한 사람이 데이터를 영구 저장소(예: 데이터베이스)에 저장하지 않은 상태에서 기기 구성 변경이 발생하게 되면 앱 사용자가 진행중이였던 작업들이 모두 날아가게 되어 안 좋은 사용자 경험을 하게 될 수 있다. 이를 방지하기 위해 구글에서는 ViewModel 이라는 Jetpack 라이브러리를 공개했다. (이 글에서는 onSaveInstaceState 에서 복원할 수 있는 Bundle 객체는 논외로 한다.)
[2]. ViewModel 사용 방법
1. ViewModel class 를 상속받는 자식 클래스를 정의한다.
class MainViewModel(private val makeLoginRequestUseCase: MakeLoginRequestUseCase) : ViewModel() {
private var _isLoginSuccess: MutableLiveData<Boolean> = MutableLiveData<Boolean>()
val isLoginSuccess: LiveData<Boolean>
get() = _isLoginSuccess
fun login(email: String, password: String) {
_isLoginSuccess.value = makeLoginRequestUseCase(email, password)
}
}
2. Activity 또는 Fragment 에서 ViewModel 객체를 생성할 때 싱글톤을 유지하기 위해 ViewModelProvider 객체에게 ViewModel 객체 생성 작업을 위임한다.
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel by lazy { ViewModelProvider(this, MainViewModelFactory(MakeLoginRequestUseCase(AuthRepository()))).get(MainViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
3. 기기 구성 변경이 발생하더라도 Activity 또는 Fragment 의 상태가 날아가지 않도록 ViewModel 내의 프로퍼티에 데이터를 저장한다.
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel by lazy { ViewModelProvider(this, MainViewModelFactory(MakeLoginRequestUseCase(AuthRepository()))).get(MainViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initialize()
}
private fun initialize() {
initViews()
initObserver()
}
private fun initViews() {
binding.loginButton.setOnClickListener {
if(binding.emailEditText.text.isNotEmpty() && binding.passwordEditText.text.isNotEmpty()) {
viewModel.login(
binding.emailEditText.text.toString().trim(),
binding.passwordEditText.text.toString().trim()
)
}
}
}
private fun initObserver() {
viewModel.isLoginSuccess.observe(this) { result ->
if(result) {
Snackbar.make(binding.root, "로그인 성공", Snackbar.LENGTH_SHORT).show()
binding.beforeLoginGroup.visibility = View.GONE
binding.afterLoginGroup.visibility = View.VISIBLE
} else {
Snackbar.make(binding.root, "로그인 실패", Snackbar.LENGTH_SHORT).show()
binding.beforeLoginGroup.visibility = View.VISIBLE
binding.afterLoginGroup.visibility = View.GONE
}
}
}
}
[3]. ViewModel 객체 생성에 관한 내용 분석
1. ViewModel 객체를 싱글톤으로 유지하기 위해 개발자가 직접 ViewModel 객체를 생성하는 것이 아닌 ViewModelProvider 클래스의 get 메서드를 호출하여 이전에 생성해둔 ViewModel 객체를 가져오거나, 이전에 생성한 적이 없다면 ViewModel 을 새로 생성한다.
@MainThread
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
//개발자가 정의한 ViewModel 클래스의 전체 경로를 가져온다.
val canonicalName = modelClass.canonicalName
?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
return get("$DEFAULT_KEY:$canonicalName", modelClass)
}
@MainThread
public open operator fun <T : ViewModel> get(modelClass: Class<T>): T {
//개발자가 정의한 ViewModel 클래스의 전체 경로를 가져온다.
val canonicalName = modelClass.canonicalName
?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
return get("$DEFAULT_KEY:$canonicalName", modelClass)
}
@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
/*
ViewModel 객체의 싱글톤을 유지하기 위해
ViewModel 을 저장, 관리하는 ViewModelStore 객체에서
동일한 key 값을 가진 ViewModel 객체를 꺼내온다.
*/
val viewModel = store[key]
/*
꺼내온 ViewModel 객체가
개발자가 요청한 ViewModel 클래스의 타입과 동일한지 검사한다.
*/
if (modelClass.isInstance(viewModel)) {
//동일한 ViewModel 클래스 타입인 경우
//ViewModel 객체를 개발자가 요구한 클래스 타입으로 형변환 하여 반환해준다.
(factory as? OnRequeryFactory)?.onRequery(viewModel)
return viewModel as T
} else {
/*
만약 꺼내온 ViewModel 객체가
개발자가 요청한 ViewModel 클래스 타입과 다르며,
동시에 null 이 아닌 경우 더 이상 진행하지 않는다.
*/
@Suppress("ControlFlowWithEmptyBody")
if (viewModel != null) {
// TODO: log a warning.
}
}
/*
ViewModelStore 에서 일치하는 ViewModel 객체를 찾지 못한 경우,
ViewModel 객체가 아직 생성되지 않았으므로 ViewModel 객체 생성 후
ViewModelStore 에 저장해 둔다.
*/
val extras = MutableCreationExtras(defaultCreationExtras)
extras[VIEW_MODEL_KEY] = key
return try {
factory.create(modelClass, extras)
} catch (e: AbstractMethodError) {
factory.create(modelClass)
}.also { store.put(key, it) }
}
[4]. ViewModel 객체 파괴에 관한 내용 분석
1. ComponentActivity 클래스에서는 현재 컴포넌트와 관련된 ViewModel 을 저장할 ViewModelStore 객체를 가지고 있으며, 현재 컴포넌트의 생명주기가 변화할 때마다 이를 감지하기 위한 옵저버를 등록한다. 만약 현재 컴포넌트의 생명주기가 ON_DESTROY 상태가 된다면 기기 구성 변경에 의한 파괴인지 아니면 다른 이유에 의한 파괴인지를 판단하여 ViewModelStore 를 유지할 것인지, 정리할 것인지를 결정한다.
@NonNull
@Override
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
/*
ViewModelStore 객체를 저장하고 있는 필드가 null 일 경우에는
새로운 ViewModelStore 객체를 생성하는 작업을 수행한다.
*/
ensureViewModelStore();
return mViewModelStore;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void ensureViewModelStore() {
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
}
public ComponentActivity() {
Lifecycle lifecycle = getLifecycle();
//noinspection ConstantConditions
if (lifecycle == null) {
throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
+ "constructor. Please make sure you are lazily constructing your Lifecycle "
+ "in the first call to getLifecycle() rather than relying on field "
+ "initialization.");
}
...
//ComponentActivity 클래스의 생성자 내에서 현재 컴포넌트의 생명주기가 변화할 때 마다 감지할 옵저버를 등록해둔다.
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
/*
만약 현재 컴포넌트의 생명주기가 ON_DESTROY 로 변화했다면
기기 구성 변경이 아닌 경우
현재 컴포넌트의 ViewModelStore 객체의 clear() 메서드를 호출하여 ViewModel 객체를 정리한다.
*/
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
...
}
[5]. ViewModelFactory 의 개념
1. ViewModelProvider 객체의 생성자에 Lifecycle Owner 만 전달하게 되면 ViewModelProvider 는 내부적으로 factory 필드에 AndroidViewModelFactory 객체를 등록하기 때문에 개발자가 get 메서드를 통해 ViewModel 객체 생성을 요청하면 AndroidViewModelFactory 객체를 사용하여 ViewModel 객체가 생성되게 된다. 그런데 AndroidViewModelFactory 객체를 통해 ViewModel 객체를 생성하게 된다면 ViewModel 객체 생성 시 매개변수를 전달할 수 없게 된다. 이러한 문제점으로 인해 개발자가 직접 ViewModelProvider.Factory 인터페이스를 구현한 Factory 객체를 ViewModelProvider 생성자에 전달하면 ViewModel 객체를 생성할 때 기본 Factory 인 AndroidViewModelFactory 를 사용하지 않고, 개발자가 정의한 Factory 를 사용하여 ViewModel 객체를 생성하기 때문에 ViewModel 객체 생성 시 매개변수를 전달할 수 있게 된다.
public open class ViewModelProvider
@JvmOverloads
constructor(
private val store: ViewModelStore,
private val factory: Factory,
private val defaultCreationExtras: CreationExtras = CreationExtras.Empty,
) {
...
//만약 ViewModelProvider 생성자에 Factory 객체를 명시적으로 전달하지 않는다면
//기본적으로 AndroidViewModelFactory 객체를 사용하게 된다.
public constructor(
owner: ViewModelStoreOwner
) : this(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))
/**
* Creates `ViewModelProvider`, which will create `ViewModels` via the given
* `Factory` and retain them in a store of the given `ViewModelStoreOwner`.
*
* @param owner a `ViewModelStoreOwner` whose [ViewModelStore] will be used to
* retain `ViewModels`
* @param factory a `Factory` which will be used to instantiate
* new `ViewModels`
*/
public constructor(owner: ViewModelStoreOwner, factory: Factory) : this(
owner.viewModelStore,
factory,
defaultCreationExtras(owner)
)
...
}
[6]. 개발자 정의 ViewModelFactory 를 사용하여 ViewModel 객체 생성
1. ViewModelProvider.Factory 인터페이스를 구현한 Factory 클래스를 정의한다. override 된 create 메서드는 ViewModelProvider 객체의 get 메서드 내부에서 호출된다.
class MainViewModelFactory(private val makeLoginRequestUseCase: MakeLoginRequestUseCase) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(makeLoginRequestUseCase) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
2. ViewModelProvider 생성자에 개발자가 직접 정의한 Factory 객체를 전달한다.
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel by lazy {
ViewModelProvider(this, MainViewModelFactory(MakeLoginRequestUseCase(AuthRepository()))).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
}
3. ViewModelProvider 객체의 get 메서드를 호출하면 내부적으로 개발자가 직접 정의한 Factory 를 통해 ViewModel 객체가 생성되게 된다.
@Suppress("UNCHECKED_CAST")
@MainThread
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T {
val viewModel = store[key]
if (modelClass.isInstance(viewModel)) {
(factory as? OnRequeryFactory)?.onRequery(viewModel)
return viewModel as T
} else {
@Suppress("ControlFlowWithEmptyBody")
if (viewModel != null) {
// TODO: log a warning.
}
}
val extras = MutableCreationExtras(defaultCreationExtras)
extras[VIEW_MODEL_KEY] = key
//개발자가 정의한 Factory 객체의 create 메서드가 호출되게 된다.
//create 메서드 내에서 ViewModel 객체를 생성하여 반환해주기 때문에 ViewModel 객체의 생성자에 매개변수를 전달하는 것이 가능하게 된다.
return try {
factory.create(modelClass, extras)
} catch (e: AbstractMethodError) {
factory.create(modelClass)
}.also { store.put(key, it) }
}'android > Jetpack' 카테고리의 다른 글
| 5-1. Jetpack Navigation의 구성요소 (0) | 2022.10.09 |
|---|---|
| 4-2 단방향 데이터 바인딩, 양방향 데이터 바인딩 (0) | 2022.10.08 |
| 4-1 DataBinding (1) | 2022.10.07 |
| 3. LiveData (0) | 2022.10.03 |
| 1. ViewBinding (1) | 2022.10.01 |