본문 바로가기

[Android/Kotlin] AAC #4 - ViewModel

AAC - ViewModel

 

 

AAC 연관 글

 

 

INDEX

 

 

 

ViewModel ?

  • ViewModel 클래스는 UI관련 데이터를 저장하고 관리하기 위해 설계
  • 시스템으로 인해 UI 컨트롤러(view)가 destroy 하거나 re-creates를 한다면 별도 저장하지 않은 데이터는 소멸

    만약 화면 회전으로 인해 re-create 발생 시 activity는 데이터를 다시 re-fatch 해야합니다
  • onSaveInstanceState()로 onCreate()에서 데이터를 복구하면 되잖아 ?

: onSaveInstanceState()의 Bundle은 Bitmap 또는 List형식의 많은 양의 데이터가 아닌

serialized, deserialize가 가능한 작은 데이터의 사용에 적합한 방법

  • 그 외에, UI 컨트롤러가 리턴을 받기위해 상당시간 소요되는 비동기방식의 Call이 빈번하게 일어날 경우

: 메모리 누수를 피하기 위해 시스템에서 Call을 정리

이런 관리는 많은 유지관리가 필요하고, 상태변화로 객체를 재 생성하는 경우, 다시 호출하는 리소스 낭비

  • UI 컨트롤러(Activity,Fragment)는 UI 데이터 표시, 사용자 이벤트 반응, 퍼미션 요청의 처리가 적합

: UI 컨트롤러에서 DB 또는 Network 작업을 한다면 무거운 클래스가 되어 과한 책임을 가짐 (테스트 어려움)

그래서 ViewModel을 통해 UI 컨트롤러 로직에서 데이터 소유권을 분리시켜야 합니다

  • Android Architecture Components(이하 AAC)에서는 ViewModel이라는 UI컨트롤러 위한 헬퍼 클래스 제공

: 즉, ViewModel은 UI를 위한 데이터를 준비하는 역할

ViewModel 객체는 화면 회전같은 상태변화에도 유지되고 다음 activity 또는 fragment 인스턴스에 즉시 사용

: LifecycleOwner가 되는 대상이 Finished가 되기까지 Recreate에도 ViewModel은 소멸되지않고 유지

ViewModel은 ScopeOwner가 Destroyed 상태가 되면 onCleared()를 호출하여 ViewModel을 Release합니다

 

ViewModel 자세히 알아보기

  • AAC의 ViewModel 라이브러리는 내부적으로 프래그먼트를 사용
  • ViewModel은 추상클래스(abstract)로 상속만으로 ViewModel클래스 생성가능

: 추상클래스이므로 new() 생성자함수로 객체 생성이 불가

ViewModelProvider를 통해 객체를 생성 

  • 생명주기 함수 onCleared()

: ScopeOwner가 Destroy 상태가 되면 onCleared() 호출해서 ViewModel을 Release

  • 커스텀 생성자 사용 시 ViewModelProvider.Factory 인터페이스 사용

  • ViewModel은 OwnerScope내에서 싱글톤(SingleTon) 객체처럼 사용이 가능

// ViewModel 상속 클래스
class MainViewModel : ViewModel() {
    var user = User()

    override fun onCleared() {
        // Release Logic
        super.onCleared()
    }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// A 액티비티의 ViewModel
class ActivityA : AppCompatActivity() {
    ...
    var viewModel = ViewModelProvider(this)[MainViewModel::class.java]
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// B 액티비티의 ViewModel
class ActivityB : AppCompatActivity() {
    ...
    var viewModel = ViewModelProvider(this)[MainViewModel::class.java]
}


: 예시를 보면 두 Activity
(이하 A,B)가 같은 ViewModel(MainViewModel)을 인스턴스로 생성했습니다

A와 B는 서로 같은 ViewModel을 인스턴스화 했지만, 두 ViewModel은 서로 다른 싱글톤 객체로 보면됩니다

ViewModelStoreOnwer로 설정된 ActivityA  ActivityB가 서로 다르기 때문에 그렇습니다

당연히 다른 액티비티에서 구현을 했으니 서로 다른 객체로 보면되겠지 싶지만 싱글톤처럼 사용이 가능하단 말은

여러 프래그먼트가 동일한 ActivityScope로 ViewModel 생성할 경우 싱글톤 같구나 이해가 될겁니다

 

ViewModel 주의사항

  • ViewModel 내부에 Activity / Fragment / View의 컨텍스트(Context)를 저장 X

: ViewModel의 Lifecycle은 외부에 존재하기때문에, 메모리 릭의 원인이 될 수 있습니다

ApplicationContext는 저장해도 상관없음 -> 별도로 AndroidViewModel 클래스를 제공

  • ViewModel은 기기의 구성변경에만 유지

: 구성변경(Configuration Change) ? 화면회전(recreate) 같은 상황

백버튼, 최근목록 앱 종료와 같은 상황에는 어떠한 처리도 기대 X

  • A액티비티의 ViewModel을 B액티비티에서 사용하고 싶다면?

: ViewModelProvider.Factory를 싱글톤(SingleTon)으로 ViewModel구현

위 방법은 다른 생명주기에서 ViewModel객체를 유지하는 것 (안티패턴, 비추천)

ViewModel 인스턴스를 유지하는게 아닌 DataSource  Repository를 싱글톤으로 공유하는 것 추천

  • 단일 액티비티에서 여러개의 프래그먼트들이 값을 공유할 때

: 프래그먼트에서 ViewModel을 생성 시 FragmentScope 대신 ActivityScope로

Activity의 ViewModel을 싱글톤처럼 서로 공유해서 사용

 

Gradle 설정

dependencies {
    def lifecycle_version = "2.2.0"
    ...

    // ViewModel and LiveData
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
}

 

ViewModel 생성방법

  • Lifecycle-extension 라이브러리가 2.2.0으로 버전업하면서 ViewModelProviders는 Deprecate되었습니다
     

구버전 - Implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'

var viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)


신버전 - Implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

var viewModel = ViewModelProvider(this).get(MainViewModel::class.java)


'2.2.0'
버전부터 ViewModelProviders클래스가 통째로 Deprecate되었습니다.

사실 ViewModelProviders는 내부적으로 ViewModelProvider()를 재호출하는 루틴이었고, 사용방법은 동일 

 

// Activity에서 생성방법
ViewModelProvider(this).get(MainViewModel::class.java)

// Fragement에서 생성방법 - 부모 ActivityScope사용
ViewModelProvider(fragment.requireActivity()).get(MainViewModel::class.java)

// Fragement에서 생성방법 - fragmentScope 사용
ViewModelProvider(fragment).get(MainViewModel::class.java)

 

 

ViewModelProvider.Factory

: AAC의 ViewModel 추상클래스를 상속해서 정의한 ViewModel 클래스는 개발자가 직접 생성자 메서드로

인스턴스 생성이 불가능합니다. ViewModelProvider를 사용해서 인스턴스를 생성할 수 밖에 없습니다

Factory클래스는 Provider에서 ViewModel 인스턴스 생성에 필요로 합니다

  

ViewModelProvider를 통해 ViewModel 인스턴스를 생성하는 여러 방법들입니다

 


#01 파라미터가 없는 ViewModel - Lifecycle Extensions

: 가장 기본으로 사용하는 편한방법으로, androidx:lifecyclelifecycle-extensions 모듈을 가져와 사용합니다

ViewModel 클래스 정의 (AAC ViewModel 추상클래스 상속)

// 파라미터가 없는 ViewModel class
class NoParamViewModel : ViewModel()
/* use ViewModelProvider's constructor provided from lifecycle-extensions package */
var noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)


ViewModelProvider()
생성자에 this를 선언하고, get() 메서드에 생성하려는 뷰모델 Type을 넣어주면 됩니다 

(this는 ViewModelStoreOwner 타입으로 Activity 또는 Fragment를 넣으면 됩니다)

 

ViewModelProvider 생성자의 다형성 구조로 Factory를 생략가능합니다

// 생성자 1, (파라미터 - ViewModelStroeOwner)
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
    // this는 ViewModelProvider 생성자함수를 의미
    this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
    	? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
        : NewInstanceFactory.getInstance());
}

// 생성자 2, (파라미터 - ViewModelStoreOwner, Factory)
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
}

// 생성자 3, (파라미터 - ViewModelStore, Factory)
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    mViewModelStore = store;
}

: 위에서 호출한 생성자는 1번 생성자를 호출하는 경우입니다.

생성자의 내부를 보면 this() 메서드로 ViewModelProvider 생성자를 다시 재호출하게 됩니다

대신 this()의 파라미터를 보면 인자로 받은 owner의 ViewModelStore를 가져오고 Factory를 생성해서 재호출 하게 되어 3번의 생성자를 호출하는 방식입니다

사용자가 Factory를 추가하여 생성자를 호출하게 되면 2번의 생성자로 호출되어 -> 3번 생성자를 재호출하게 됩니다

 

 


#02 파라미터가 없는 ViewModel - ViewModelProvider.NewInstanceFactory

: NewInstanceFactory는 안드로이드에서 기본적으로 제공하는 팩토리 클래스

ViewModelProvider.Factory 인터페이스를 구현한 클래스

ViewModel 클래스가 파라마미터를 필요로 하지 않거나, 커스텀할 필요가 없는 상황에서 1번아니면 2번 방법을 사용합니다

noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
		.get(NoParamViewModel::class.java)


: ViewModelProvider 생성자로 ViewModelStoreOwnerFactroy를 넘겨줍니다

 

 


#03 파라미터가 없는 ViewModel - CustomFactory

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


: NoParamViewModelFactory 클래스를 정의하였습니다

Factroy 인터페이스의 추상메서드 create()를 재정의를 해줘야합니다

위 예시에서는 modelClass가 NoParamViewModel클래스가 맞으면 ViewModel 인스턴스를 생성하고

아니면 IllegalArgumentException() 예외를 발생시키도록 되어있습니다 

 

그런데 create()는 언제호출되고 create()의 파라미터 modelClass는 뭘까 이해가 되지않습니다

var factory = NoParamViewModelFactory()

var viewModel = ViewModelProvider(this, factory).get(NoParamViewModel::class.java)

먼저 ViewModelProvider 생성자에 Factory 인스턴스를 넘겨주고, get()함수의 파라미터인 ViewModel Class가

Factroy의 create(modelClass)로 들어오게 됩니다

즉, Factroy의 create()메서드에서 get()의 인자인 Class와 비교를 하게 됩니다 

 

 


#04 파라미터가 있는 ViewModel - Custom Factory

: 위의 3번방법에서 CustomFactroy클래스에 파라미터를 추가하여 ViewModel 생성에 파라미터를 전달하는 방법

ViewModel.class

class HasParamViewModel(val param: String) : ViewModel()

: param이라는 String 프로퍼티를 가진 ViewModel클래스입니다

 

ViewModelFactory.class

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()
        }
    }
}

: CustomFactory클래스의 생성자로 param 프로퍼티를 받아 ViewModel 인스턴스 생성 시 전달해줍니다

 

Activity / Fragment

// Factory 방법 1
var factory = MainViewModelFactory("테스트")
var viewModel = ViewModelProvider(this, factory).get(HasParamViewModel::class.java)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// Factory 방법 2 (익명 객체)
var viewModel = ViewModelProvider(this, HasParamViewModelFactory("테스트")
		.get(HasParamViewModel::class.java)

 

 


#05 파라미터가 없는 AndroidViewModel - AndroidViewModelFactory

: AndroidViewModel 클래스는 기본제공되는 ViewModel()을 구현한 클래스입니다

일반적인 ViewModel들은 Activity / Fragment의 Lifecycle에 의존적인 뷰모델 객체이고

AndroidViewModel은 어플리케이션(Application)의 Scope에 의존적인 뷰모델입니다

즉, ApplicationScope에 의존적이기에 특정 Activity/Fragment가 Destory되어도 인스턴스가 유지되고

Application이 종료되는 시점에 onCleared()가 호출되어 뷰모델이 Release됩니다

 

AndroidViewModel.class

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


: AndroidViewModel은 ApplicationScope
에 의존적이므로 Application 객체를 전달해줘야 합니다

 

Activity / Fragment

var noParamAndroidViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))
            .get(NoParamAndroidViewModel::class.java)

 

 


#06 파라미터가 있는 AndroidViewModel

: 파라미터가 있는 AndroidViewModel 객체를 생성하는 방법입니다

 

AndroidViewModel.class

class HasParamAndroidViewModel(application: Application, val param: String)
    : AndroidViewModel(application)

 

AndroidViewModelFactory.class

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)
    }
}