본문 바로가기
Android/Jetpack

[ViewModel] ViewModel 시작하기

by jaesungLeee 2022. 3. 11.

1. ViewModel 개요

ViewModel은 수명주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었다. 특히, 화면 회전과 같은 구성 변경에도 데이터 유지가 가능하다.

1.1. 기존의 단점들

단순한 데이터의 경우 Activity는 onSavedState()를 사용해 onCreate()의 Bundle에서 데이터를 복원할 수 있었다. 하지만, 이 경우는 Bitmap과 같은 대량의 데이터에는 적합하지 않다.

 

View에서는 비동기 호출을 자주 한다. View는 비동기 호출을 관리하며 많은 유지관리가 필요하고, 화면 회전과 같은 구성 변경 시 개체가 다시 생성되는 경우 이미 수행한 호출을 다시 호출하기 때문에 리소스 낭비가 생기게 된다.

 

View는 UI 표시, 사용자 상호작용, 권한 요청과 같은 역할만 처리해야 한다. 이 때, 데이터 베이스나 네트워크 같은 데이터 로딩 작업은 하지 않아야 한다. 테스트가 어려워지기 때문이다. 즉, View는 정확히 View의 역할만 해야하고, View 로직에서 데이터 관련 작업은 분리하는 것이 좋다.

 

1.2. 의존성 추가

dependencies {
    def lifecycle_version = "2.5.0-alpha01"  // Jan.26 2022
    def arch_version = "2.1.0"

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

    // Saved state module for ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

    // kapt
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"

    // optional - helpers for implementing LifecycleOwner in a Service
    implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"

    // optional - Test helpers for LiveData
    testImplementation "androidx.arch.core:core-testing:$arch_version"
}

2. ViewModel 생명 주기

Activity의 생명 주기에 관계없이 ViewModel이 해제되지않고 살아있다면 데이터가 계속 유지되어 데이터 손실을 방지할 수 있다. 즉, Activity의 생명 주기 보다 더 긴 생명 주기를 갖는다. 이 말은 ViewModel을 사용하면 Activity의 생명주기에서 자유롭기 때문에 화면 전환 시 데이터 소멸을 방지할 수 있다.

 

ViewModel의 생명주기인 Scope는 ViewModelProvider에 의해 결정된다. 정확히는 ViewModelProvider 생성자 함수의 파라미터로 전달하는 ViewModelStoreOwner의 생명 주기를 따른다고 볼 수 있다.

 

Activity는 Create, Rotate, Finish되면서 다양한 생명 주기 상태를 갖는 반면 ViewModel의 생명주기는 변하지 않는다. ViewModel은 Activity가 더 이상 사용하지 않는 상태인 onDestroy() 호출 이후 onCleared()을 호출하여 내부 데이터를 초기화하고 해제하게 된다.


3. ViewModel Process

ViewModel() 클래스를 상속하여 정의한 ViewModel 클래스는 직접 생성자를 통하여 인스턴스를 생성할 수 없다. 따라서, ViewModelProvider.Factory 인터페이스를 필요로 한다.

 

3.1. ViewModelProvider

ViewModelProvider의 내부 코드는 아래와 같다.

public open class ViewModelProvider(
    private val store: ViewModelStore,
    private val factory: Factory
) {
    public interface Factory {
    	public fun <T : ViewModel> create(modelClass: Class<T>): T
    }

    ...
		
    public constructor(owner: ViewModelStoreOwner) : this(owner.viewModelStore, defaultFactory(owner))
		
    ...
    
    public constructor(
    	owner: ViewModelStoreOwner, 
        factory: Factory
    ) : this(owner.viewModelStore, factory)
    
    ...
}

ViewModelProvider는 ViewModel 객체를 생성하기 위해 사용하는 클래스이다. 다른 말로 Scope에 ViewModel들을 제공하는 Utility Class로 볼 수 있다. Activity나 Fragment에서는 ViewModelProvider를 통해 자기 자신을 생성자로 전달하여 ViewModel 인스턴스를 획득해야 한다. ex. ViewModelProvider(MyActivity)

 

3.2. ViewModelStoreOwner

ViewModelStoreOwner의 내부 코드는 아래와 같다.

public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

FragmentActivity의 부모 클래스인 ComponentActivity와 Fragment 클래스가 ViewModelStoreOwner를 구현한다. 즉, Activity와 Fragment가 ViewModelStoreOwner를 구현하기 때문에 ViewModel 객체를 생성할 때 Activity나 Fragment를 생성자로 전달하게 되는 것이다.

 

어떤 Owner를 통해 생성하냐에 따라 ViewModel의 Scope가 정해진다. ViewModelStoreOwner를 구현하는 것은 ViewModelStore를 유지하고, Scope가 Destroy될 때 ViewModelStore.clear()를 호출하는 책임을 갖게 된다.

 

3.3. ViewModelStore

ViewModelStore의 내부 코드는 아래와 같다.

public class ViewModelStore {

    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 클래스는 내부적으로 HashMap<String, ViewModel> 를 두어 ViewModel을 관리한다. ViewModelStore는 화면 회전시에도 유지되는 인스턴스이다. ViewModel의 Owner인 Activity나 Fragment가 화면 회전에 의해 Destroy되고 Rotate 되어도 Owner의 새로운 객체는 여전히 같은 ViewModelStore를 갖게 된다. 즉, Owner의 생명 주기가 완전히 Destroy되지 않는 이상 동일한 ViewModelStore를 가지고, ViewModelStore가 ViewModel을 관리하기 때문에 데이터 손실이 없는 것이다.

 

ViewModelStoreViewModelStoreOwner에서 생성하고 관리한다.


4. ViewModel 구현

4.1. ViewModel 클래스 정의

ViewModel 객체는 구성 변경되는 동안 자동으로 보관된다. 이 객체가 보유한 데이터는 다음 Activity 또는 Fragment 인스턴스에서 즉시 사용 가능하다.

 

아래는 ViewModel 클래스를 정의하는 한 가지 예시 코드이다.

class MyViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData<List<User>>().also {
            loadUsers()
        }
    }

    fun getUsers(): LiveData<List<User>> {
        return users
    }

    private fun loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

 

4.2. ViewModel 초기화 방법

ViewModelProviders - Deprecated

기존에는 아래와 같이 사용하기도 했다.

// Deprecated in AndroidX Lifecycle 2.2.0-alpha

val viewModel = ViewModelProviders.of(this).get(AnyViewModel::class.java)

ViewModelProviders를 사용하여 ViewModel의 인스턴스를 생성하는 방법은 현재 Deprecated 되었다. ViewModelProviders 대신 ViewModelProvider를 사용하는 것을 권장하고 있다.

 

ViewModelProvider

ViewModel의 생성자의 Parameter가 있냐 없냐에 따라 생성 방법이 다르다. 보통 여기서 Parameter는 Repository에 해당한다.

 

  1. ViewModel의 생성자에 Parameter가 없는 경우
    : ViewModelProvider로 ViewModel 인스턴스를 생성할 수 있다.
  2. ViewModel의 생성자에 Parameter가 있는 경우
    : ViewModelProvider.Factory 클래스를 구현하면 ViewModelProvider 생성자에 Parameter를 전달할 수 있다.

 

1. Parameter가 없는 ViewModel

class NoParamViewModel : ViewModel()

class MainActivity : AppCompatActivity() {
	
    private lateinit var noParamViewModel: NoParamViewModel()

    override fun onCreate(..) {
        ..
				
        noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
    }
}

ViewModelProvider의 생성자인 thisViewModelStoreOwner를 의미한다. Activity가 ViewModelStoreOwner 인터페이스를 구현하고 있기 때문에 this를 사용한다고 볼 수 있다.

 

get()안에 생성하고자 하는 ViewModel 클래스를 넣는다.

 

2. Parameter가 없는 ViewModel - ViewModelProvider.NewInstanceFactory

class NoParamViewModel : ViewModel()

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamViewModel: NoParamViewModel
 
    override fun onCreate(..) {
        ..
 
        noParamViewModel = ViewModelProvider(
                this, 
                ViewModelProvider.NewInstanceFactory()
        ).get(NoParamViewModel::class.java)
    }
}

 

3. Parameter가 없는 ViewModel - ViewModelProvider.Factory

class NoParamViewModel : 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()
        }
    }
}

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamViewModel: NoParamViewModel
 
    override fun onCreate(..) {
        ..
 
        noParamViewModel = ViewModelProvider(
                this, 
                NoParamViewModelFactory
        ).get(NoParamViewModel::class.java)
    }
}

직접 ViewModelProvider.Factory 인터페이스를 구현하여 사용하는 방법이다. 하나의 Factory로 다양한 ViewModel 클래스를 관리할 수 있는 장점이 있다.

 

4. Parameter가 있는 ViewModel - ViewModelProvider.Factory

class FirstActivityViewModel(
    private val repository: MyRepository
) : ViewModel() {

    val result = MutableLiveData<String>("init value")

    fun reloadResults() {
        result.value = repository.getResult()
    }
}

class ViewModelFactory(private val repository: MyRepository)
    : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(FirstActivityViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return FirstActivityViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

ViewModelProvider.Factory를 직접 구현하면 해당 Parameter를 소유하는 ViewModel 인스턴스를 생성할 수 있다.

 

ViewModel을 생성할 때는 Factory 객체를 생성하고, ViewModelProvider에 Factory를 전달하여 생성한다.

val repository = MyRepository()
val viewModelFactory = ViewModelFactory(repository)

val viewModel = ViewModelProvider(
    this, 
    viewModelFactory
).get(FirstActivityViewModel::class.java)

5. AndroidViewModel

ViewModel 클래스를 만들 때 ViewModel() 말고 AndroidViewModel()를 상속받아 구현할 수 있다. 여기서 AndroidViewModel()ViewModel()의 서브 클래스이다.

 

일반적인 ViewModel() 클래스에서 Context 객체를 소유하거나 접근하는 것을 권장하지 않는다. 불가피하게 필요한 경우 즉, ViewModel에서 Context를 사용해야 할 필요가 있는 경우에 AndroidViewModel()을 사용한다.

 

AndroidViewModel의 생성자로는 Application 객체가 필요하다. 즉, 개발자는 Application의 필요 여부에 따라 ViewModelAndroidViewModel을 선택적으로 사용할 수 있다.

 

1. Parameter가 없는 AndroidViewModel - AndroidViewModelFactory

class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application) {  }

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamAndroidViewModel: NoParamAndroidViewModel
 
    override fun onCreate(..) {
        ..
 
        noParamAndroidViewModel = ViewModelProvider(
                this, 
                ViewModelProvider.AndroidViewModelFactory(application)
        ).get(NoParamAndroidViewModel::class.java)
    }
}

ViewModelProvider의 파라미터로 AndroidViewModelFactory 객체를 함께 전달한다.

 

2. Parameter가 있는 AndroidViewModel

class ThirdActivityViewModel(
    application: Application,
    private val repository: MyRepository
) : AndroidViewModel(application) {  }

class ViewModelFactory(
    private val application: Application, 
    private val repository: MyRepository
) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ThirdActivityViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return ThirdActivityViewModel(application, repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

ViewModelProvider.NewInstanceFactory를 상속하여 구현할 수도 있다.

 

ViewModel을 생성할 때도 Factory 객체를 ViewModelProvider에 전달하여 생성한다.

val repository = MyRepository()
val viewModelFactory = ViewModelFactory(application, repository)

val viewModel = ViewModelProvider(
    this, 
    viewModelFactory
).get(ThirdActivityViewModel::class.java)

 

6. Android KTX를 이용한 ViewModel 초기화

Android KTX 라이브러리는 viewModelScope() 함수를 제공한다. viewModelScope()는 코루틴을 실행하기에 적합하다. CoroutineScope는 Dispatchers.Main에 바인딩되고, ViewModel이 해제되면 자동으로 취소된다. 즉, ViewModel에 새로운 Scope를 생성하는 것 대신 viewModelScope()를 사용할 수 있다.

 

또한, Kotlin의 by 키워드를 이용하여 사용할 수 있다.

6.1. 의존성 추가

dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha02")

    implementation "androidx.activity:activity-ktx:1.4.0"
    implementation "androidx.fragment:fragment-ktx:1.4.1"
}

 

6.2. by ViewModels<>()

class MainViewModel : ViewModel() {
    // Make a network request without blocking the UI thread
    private fun makeNetworkRequest() {
        // launch a coroutine in viewModelScope
        viewModelScope.launch  {
            remoteApi.slowFetch()
            ...
        }
    }

    // No need to override onCleared()
}

private val mainViewModel by viewModels<MainViewModel>()

KTX를 사용하여 ViewModel 초기화 시 ViewModelProvider를 통한 Factory 인터페이스를 구현하지 않아도 된다.

 

by viewModels()를 이용한 초기화는 해당 ViewModel이 초기화되는 Activity 혹은 Fragment의 생명 주기에 종속된다.

 

6.3. SharedViewModel with by activityViewModels()

Activity에 올라가있는 Fragment 간 데이터 공유가 필요한 경우에 사용되는 ViewModel이다.

 

아래와 같이 Activity와 Fragment간의 공동의 ViewModel을 두어 Fragment 간에 데이터 공유를 하는 컨셉을 생각해볼 수 있다.

 

SharedViewModel은 아래 방식대로 하면 같은 인스턴스를 가리키게 되고 공유된다. MainActivity에서는 by viewModels()로 초기화 한다. Fragment에서는 by activityViewModels()로 초기화 한다.

 

즉, 하나의 Activity에 붙은 여러 Fragment들은 Activity에 종속되는 ViewModel을 생성함으로써 데이터를 공유 및 전달할 수 있다.

 

ViewModelProvider로 SharedViewModel 구현

private val sharedViewModel by lazy {
    ViewModelProvider(requireActivity(), object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return  MainViewModel() as T
        }
    }).get(MainViewModel::class.java)
}

ViewStoreOwnerrequireActivity()로 Activity를 파라미터로 넘긴다.

 

References

https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko#kotlin

https://velog.io/@changhee09/안드로이드-ViewModel

https://readystory.tistory.com/176

https://velog.io/@ashwon1218/Android-JetPack-ViewModel이란-무엇인가

https://choheeis.github.io/newblog//articles/2021-02/viewModel

https://codechacha.com/ko/android-jetpack-create-viewmodel/

https://black-jin0427.tistory.com/389

https://black-jin0427.tistory.com/322

https://skynight1996.medium.com/navigation-component-comparison-between-viewmodels-activityviewmodels-and-ae0145734228

https://kotlinworld.com/88?category=971011

'Android > Jetpack' 카테고리의 다른 글

[Room] SQLite에서 Room으로  (0) 2021.10.06