ViewModel

나고수·2022년 3월 21일
0

1일1공부

목록 보기
24/68
post-custom-banner

ViewModel의 정의와 쓰는이유는 생략 (공식문서참고)
참고블로그1
참고블로그2
참고블로그3
참고블로그4

생명주기

val viewModel = ViewModelProvider(this).get(UserModel::class.java)

ViewModelPrivider(여기에 들어가는 Activity/Fragment의 lifeCycle을 따름)

  • why?
    ViewModelProvider의 파라미터로 ViewModelStoreOwner이 들어간다. ViewModelStoreOwner은 viewModelStore을 관리하는 인터페이스이다. ViewModelStore은 HashMap<String,viewModel> 형태로 viewModel을 관리하는 객체이다.

    activity와 fragment에 viewModelStoreOwner가 구현되어있기 때문에 viewModelProvider의 파라미터로 들어 갈 수 있다.

    따라서, viewModel은 viewModelProvider의 파라미터로 들어가는 activity나 fragment의 생명주기를 따른다. 라는 것을 알 수 있다.
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());

같은 viewModelStoreOwner에 대해 같은 이름의 뷰모델을 생성하면, 같은 인스턴스가 나온다.

  • why?
    - viewModel은 viewModelStoreOwner에 대해 싱글톤으로 만들어짐
    - 어케 아나요? : 밑에 파라미터가 없는경우 1 초기화 설명 참고

초기화 시점

초기화 시점 :
activity - onCreate() 이후 ,
fragment - onAttach() <프래그먼트가 액티비티에 붙은> 이후

  • why?
    - 일반적으로 시스템에서 활동 객체의 onCreate() 메서드를 처음 호출할 때 ViewModel을 요청합니다. 라고 공식문서가 말했으니까..?

    viewModelStoreOwner을 구현한 activity/fragment의 생명주기를 따르니까, 당연히 activity가 onCreate()된 이후/fragment(는 activity의 생명주기를 따르므로)가 onAttached()된 이후에 초기화 하는게 맞지 않을까.
    activity가 create 되지도 않았는데, activity의 생명주기를 따르는 viewModel을 먼저 초기화 하는건 좀 이상하잖..

초기화 방법

viewModelProvider을 통해 초기화 : viewModelProvider 의 파라미터로 들어오는 activity나 fragment의 생명주기를 따른다.

hilt를 사용하면 개쉽다. 이것도 공부해서 다시 포스팅하겠슴.🤷‍♀️

  //파라미터가 없는 경우 1
val noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
//.get()함수 설명 
/**
 * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
 * an activity), associated with this ViewModelProvider.
 * 
 * The created ViewModel is associated with the given scope and will be retained
 * as long as the scope is alive (e.g. if it is an activity, until it is
 * finished or process is killed).
 * ViewModelProvider의 생명주기에 맞춰 이미 뷰모델이 존재하면 그것을 리턴, 
 * 뷰모델이 없으면 만들어서 리턴 하는 함수 인듯.
 * @param modelClass The class of the ViewModel to create an instance of it if it is not present. 
 //get의 인자로 오는 뷰모델
 * @param <T> The type parameter for the ViewModel.
 * @return A ViewModel that is an instance of the given type T
 */
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    //getCanonicalName 전체 경로명의 클래스명 
    if (canonicalName == null) {
        throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
    } 
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass); 
    //키, 벨류로 뷰모델 저장. 뷰모델이 없으면 만들어서 저장하고 있으면 이미 있는걸 리턴하는듯.
}
  //파라미터가 없는 경우 2 - 안드로이드에서 제공해주는 기본 팩토리 사용 
  //ViewModelProvider.NewInstanceFactory
  //extends Object
  //implements ViewModelProvider.Factory
val noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
            .get(NoParamViewModel::class.java)
//파라미터라 없는 경우 3 - 직접 팩토리클래스 만들기 
//하나의 팩토리로 여러 상황을 컨트롤 할 수 있음.
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()
        }
}//ㅤget의 파라미터(==NoParamViewModel 클래스)가 viewModelProvider의 factory(==NoParmaViewModelFactory)의 create의 파라미터(==modelClass)로 들어간다. 
//즉, NoParamViewModel이 NoParamViewModel을 상속하거나 구현하였으면(???🤷‍♀️) NoParamViewModel()을 리턴
var factory = NoParamViewModelFactory()var viewModel = ViewModelProvider(this, factory).get(NoParamViewModel::class.java)
// 파라미터가 있는 뷰모델 - 직접 팩토리클래스 만들기 
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()
        }
    }
}class HasParamViewModel(val param: String) : ViewModel()
// 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)
/*ViewModel 에서 Context 를 사용 . 냅다 액티비티나 프래그먼트의 context를 가져다 쓰면 안됩니다! - AndroidViewModel / ViewModelProvider.AndroidViewModelFactory 사용*/
class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)
var noParamAndroidViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))
            .get(NoParamAndroidViewModel::class.java)
//viewModel 에서 context 사용 & 뷰모델에 파라미터 있을 때
class HasParamAndroidViewModel(application: Application, val param: String)
    : AndroidViewModel(application)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)
    }
}    

코틀린 대리자 by 를 이용한 초기화

// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
//액티비티와 프래그먼트 둘다에서 쓸 수 있으며, 초기화 되는 액티비티/프래그먼트의 생명주기를 따른다.
        val model: MyViewModel by viewModels()

Fragment에서만 가능한 초기화 - byActivityViewModels()

  • Framgent가 속한 Activity의 생명주기를 따른다.
    만약 같은 액티비티에 속한 a,b 프래그먼트에서 byActivityViewModels()로 뷰모델을 만들었다면, 두 프래그먼트에서 같은 뷰모델을 공유하는 것이다.
  • why?
    viewModelStore(여기서는 프래그먼트가 붙은 액티비티)에 싱글톤으로 하나의 뷰모델이 만들어졌을 것이고, byActivityViewModels()로 그 뷰모델을 쉐어하고 있는 것이기 때문에 두 프래그먼트에서 같은 뷰모델 인스턴스가 리턴된다!
  • if) 두 프래그먼트에서 따로 뷰모델을 쓰고 싶다면, byActivityViewModels() 방법이 아니라 그냥 각각의 프래그먼트에서 by viewModels()로 뷰모델을 각각 만들면 된다.
class DetailFragment : Fragment() {// Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

viewModel에서 왜 context를 참조하면 안되는지?

일단 여기서는 activity가 Context를 확장한 클래스 라는 것만 짚고 넘어갑시다.
그렇기 때문에, context는 activity의 생명 주기를 따릅니다.

  • 문제는 viewmodel은 activity보다 생명주기가 길다는 점에 있습니다.
    viewmodel에서 냅다 activity의 context를 갖다 썼다고 해봅시다.

    예를들어, 화면이 회전되서 activity가 destroy 되고 다시 create 될 때, context는 액티비티의 생명주기에 따라 같이 destroy되고 재생성 될것입니다.
    하지만 viewmodel은 화면이 회전되든 말든 activity가 완전히 destroy 되기 전까지 살아있습니다.
  • 따라서 화면 회전 후 activity의 context는 새로 만들어진 액티비티의 context 지만, viewmodel에 저장된 context는 예전 context 일 것입니다.

    이처럼 viewmodel과 acticity(context)의 생명주기차이 (정확히 말하자면, viewmodel의 생명주기가 activity의 생명주기보다 길기때문에) 때문에 viewmodel에서 냅다 activity의 context 를 갖다써버리면 서로가 가지고 있는 context가 불일치 하는 상황이 생겨버립니다.

    따라서, viewmodel 에서 context를 쓰고싶다면, 안드로이드에서 제공하는 androidViewModel과 ViewModelProvider.AndroidViewModelFactory을 사용해야 합니다.

MVVM 의 ViewModel vs AAC ViewModel

참고블로그
간단히 말하자면,

  • MVVM의 viewModel은 'view 와 로직(viewmodel에)을 구별 하고, viewmodel의 변화를 view에서 관찰 한 후 view를 업데이트 함'
  • AAC의 viewModel은 '수명주기에 따라 액티비티가 destory 되도 데이터를 유지할 수 있게 하는 요소'
  • 둘은 아예 다른 개념이다.
  • AAC의 viewModel을 쓴다해서 MVVM 패턴이 되는게 아니다.
  • 하지만 헷갈리는 이유는 ... 안드로이드 MVVM 패턴을 서칭해보면, 다 AAC viewModel을 사용하고 있기 때문이다.
  • 하지만 그 블로그들이 틀린건 아니다.
    왜냐면, AAC viewModel에 liveData를 써서 view 에서 observing 한다면 그것은 MVVM viewModel 패턴이 되는 것이기 때문에.
profile
되고싶다
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 3월 21일

aac viewmodel vs mvvm viewmodel

답글 달기