Viewmodel은 무엇인가 생각보다 심오하다. 사용법이 심오한건가?
어쨋든 개념부터 살펴보자
뭐 일단 MVVM 의 ViewModel과 AAC의 ViewModel은 엄연히 다른것이다.
MVVM의 ViewModel 설명하려면 날새야하고 그걸 생략하고 우리가 다룰 AAC의 ViewModel은
MVVM의 ViewModel을 잘만들라고 구글에서 안드로이드의 특성에 맞도록 보조적인 장치들을 달아서 만들어준 느낌이다.
그래서 ACC의 ViewModel을 썻다고 사실 MVVM 에서 요구하는 ViewModel의 조건을 충족하지 못할 수 있다. 여타 많은 DataBinding이라던지 다양한 작업을 해줘야 MVVM의 ViewModel을 충족할수있다.
그래서 결국에 AAC ViewModel 에서 제공하는게 뭔데 씹덕아 하고 물어본다면
기존에 Activity나 Fragment 의 생명주기를 따르지만 세분화 되어있던게 하나로 묶여서 생명주기가 시작될때
생성되어서 Finished되는 시점에 사라지는 한마디로 화면회전이나 여타 조건에의해 화면이 destory되었다 다시 생성되어도 ViewModel은 여전히 메모리 상에 남아있어 화면회전과 같은것들에 영향을 받지않고 데이터를 유지할수있다.
예쁘게 말하면 이런거다
Configuration 변경이 (예:화면 회전) 발생할 때 액티비티가 다시 시작 되는 것을 확인할 수 있다. 하지만 ViewModel은 여전히 메모리 상에 남아있는다. 이는 Activity 내부에서 Configuration 변경과 무관하게 유지 되는 NonConfigurationInstances 객체를 따로 관리하기 때문이다.
출처: charlezz
그래서 ViewModel을 쓰기전에 어떤식으로 내부적으로 돌아가는지 ViewModel 요청 프로세스를 봐보자
- ViewModelProvider를 통해 ViewModel 인스턴스를 요청한다.
- ViewModelProvider 내부에서는 ViewModelStoreOwner 를 참조하여 ViewModelStore를 가져온다.
- ViewModelStore에게 이미 생성된(저장된) ViewModel 인스턴스를 요청한다.
- 만약 ViewModelStore가 적합한 ViewModel 인스턴스를 가지고 있지 않다면,
Factory를 통해 ViewModel인스턴스를 생성한다. - 생성한 ViewModel 인스턴스를 ViewModeStore에 저장하고 만들어진 ViewModel 인스턴스를 클라이언트에게 반환한다.
- 똑같은 ViewModel 인스턴스 요청이 들어온다면, 1~3번의 과정을 반복하게 된다.
자 여기서 대략적인 용어 살명을 하자면
ViewModelStoreOwner : Activity나 Fragment처럼 생명주기 를 제공하는것 ViewModelStore를 소유하고 있는 대상을 명시하기 위해 사용됨
ViewModelStore: AAC ViewModel 라이브러리에서 제공하는 또 다른 클래스. 이 클래스는 ViewModel 역할을 하는 클래스의 인스턴스들이 UI 컨트롤러의 수명 주기에 관계 없이 어딘가에 지속적으로 저장되도록 설계된 클래스.
내부의 코드를 보면 HashMap 자료구조를 사용하여 HashMap 안에 key-value 쌍으로 ViewModel들의 인스턴스를 저장하고 있음
추가설명 어렵다면 넘기자-> ViewModelStore 클래스의 인스턴스가 메모리에 남아있는 한 UI 컨트롤러 인스턴스가 메모리에서 소멸되고 다시 생성되어도 해당 UI 컨트롤러와 연결된 ViewModel 인스턴스를 HashMap에서 다시 찾을 수 있는 것입니다.(즉, ViewModel 역할을 하는 클래스에 UI 데이터를 저장할 경우 UI 데이터가 소실되지 않습니다.)
ViewModel: 실제 생성되는 인스턴스
Factory: ViewModel인스턴스 만드는곳 만약 ViewModel만들때 매개변수를 통해 외부에서 데이터 넣어주고 싶다면 커스텀해서 사용해야함
개념은 이정도 하고 실제로 사용하는 방법을 봐보자
사용법
1.gradle에 종속성부터 추가해주자
(안해도 기본적으로 들어가있는거 같은데 어디선 또 추가하고 어쨋든 추가해주자 걍)
dependencies {
val lifecycle_version = "2.4.0"
val arch_version = "2.1.0"
//2021/11/03 현재 최신
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko
버전 참고는 위링크에서 ㅎㅎ
2.ViewModeld 구현
코틀린 class파일을 하나만들어주고 ViewModle을 상속받고 필요한 내용들을 작성해준다.
예를들어 라이브데이터 가지고필요한것들을 작성해주고 그에 대한 데이터 처리함수 같은것들을 작성해준다.
package changhwan.experiment.twowaybinding
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
//ViewModel내에서 사용하는 MutableLiveData
private val _name = MutableLiveData<String>()
val name : LiveData<String>
get() = _name
//초기값 설정(필요할때 사용하겠지)
init{
_name.value = "이창환은 빡빡머리"
}
//데이터 처리하는 함수들
fun changename () {
_name.value = "이창환은 사실 빡빡머리아님"
}
}
이런식으로 필요한것들 변수(데이터)라던지 그관련 처리 로직을 작성해준다.
이때 라이브데이터 넣을꺼면 코틀린 컨벤션의 backing properties 를 사용해서 LiveData로 밖에서는 내부에 관여할수없게 막고 내부에서만 MutableLiveData를 이용하여 마음대로 수정 가능할수있게하여
Delegation pattern 에도 부합하고 캡슐화 측면에도 맞도록 만든다.
결국이렇게 데이터 관련 함수들을 따로 생명주기에 상관없이 빼서 관리하는 형태로 만들어줄 수 있도록 하는것이다.
뷰로직과 데이터 로직을 분리시키며 생명주기또한 따로 가지고있어 관리하기 쉽도록 만든형태이다.
참고적으로 뷰 로직은 뷰모델에서 처리하면 절대 안된다
->직접 참조시 데이터가 누수된다는데 이유는 뷰모델 생명주기가 뷰보다 더 길어서 그런 현상이 일어난다고 한다.
(뷰로직이 뭐냐? -> 예를 들어 바인딩같은것들 클릭리스너 같은것들
->이런게 뷰관련 로직이고 이런것들을 뷰모델로 절대 들고가면안됨)
근데 직접참조가 뭐냐 뭐이렇게 말을 어렵게해놨어!!
-> 문다빈현자의 조언
뷰모델의 기능들을 뷰에서 참조하는건 괜춘한데 뷰컴포넌트들을 뷰모델에 갖고가지 말라는 소리란다
-> 나의 반문
아 그니까 액티비티에서 만들어놓은 인스턴스 같은것들을 뷰모델로끌고가지마라? class 정의해놓은데에?
-> 답변
ㅇㅇ 마즘 아키텍쳐측면에서도 사실 그따구로하면 안됨 뷰로직과 데이터로직 분리하려고 MVVM쓰는건데
-> 나의 반문
맨날 느슨한 결합 거리는데 개빡침 너덜너덜한 결합으로 개명하자
3.필요한곳에서 인스턴스 만들어주기
이렇게 ViewModel class 작성해준후 사용하려는 Activity나 Fragment가서 실제 객체 인스턴스를 만들어줘야하는데 그
방법이 엄청나게 많다 팩토리를 이용하네 마네 어쩌구 저쩌구 일단 간단하게 보면 두가지 유형으로 나눌수있다.
그냥 만들어주기 vs 생성자로 뭐 집어넣어서 view모델 생성하기
후자의 경우 ViewModelFactory를 직접 만들어줘야한다.
어쩃든 쉬운것부터 보자
외부에서 인자를 안넣어줄 필요가 없는경우 (파라미터가 없는경우)
1. Lifecycle Extensions 많이쓸거같음
이 방법은 가장 편한 방법 중 하나입니다. androidx.lifecycle의 lifecycle-extensions 모듈을 가져와 사용하면 됩니다.
먼저, module 수준의 build.gradle 에 다음과 같이 디펜던시를 추가해줍니다.
dependencies {
// ...
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}
class MainActivity : AppCompatActivity() {
private lateinit var noParamViewModel: NoParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
/* use ViewModelProvider's constructor provided from lifecycle-extensions package */
noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
}
}
noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
요부분이 초기화 해주는건데 this는 ViewModelOwner가 들어가는거니 Activity나 Fragment가 들어가는곳이다
Fragment에서 부모 액티비티 넣고싶으면 requireActivity()넣어주면 되겠지
2. ViewModelProvider.NewInstanceFactory
한마디로 안드로이드에서 기본제공하는 팩토리를 명시해서 사용하는방법이다.
이번에 살펴볼 방법은 NewInstanceFactory 입니다.
이는 안드로이드가 기본적으로 제공해주는 팩토리 클래스이며, ViewModelProvider.Factory 인터페이스를 구현하고 있습니다. 따라서 ViewModel 클래스가 파라미터를 필요로 하지 않거나, 특별히 팩토리를 커스텀 할 필요가 없는 상황에서는 1번 방법을 사용하거나, 2번 방법을 사용하면 되겠습니다.
2번 방법은 1번에서 추가해준 lifecycle-extenstions 모듈을 추가하지 않아도 사용가능한 방법입니다.
class MainActivity : AppCompatActivity() {
private lateinit var noParamViewModel: NoParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
.get(NoParamViewModel::class.java)
}
}
3. ViewModelProvider.Factory
생성자로 뭐 안넣어줘도 팩토리 직접작성해서 그거 사용할수도있다 그냥 작성해서 위의 2번방법 NewInstanceFactory 방법에서 내가만든 팩토리로 대체 해주면되는것이다.
이 방법의 장점은 하나의 팩토리로 다양한 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()
}
}
}
위 코드는 NoParamViewModel 클래스가 아니면 IllegalArgumentException 을 던지도록 구현되어 있습니다. 이는 어디까지나 개발자의 마음대로 구현하면 되는 부분이며, 어떤 타입의 클래스가 전달되더라도 인스턴스를 생성하도록 구현할 수도 있습니다.
4. Android KTX 사용 개편함 많이 사용할듯
Android KTX는 다양한 코틀린 확장함수들을 모아놓은 라이브러리다.
[KTX] 라이브러리 추가
implementation "androidx.fragment:fragment-ktx:1.3.4"
Activity, Fragment 에서의 초기
private val mainViewModel by viewModels<MainViewModel>()
by viewModels 키워드를 사용하여 간편하게 초기화해 줄 수 있다. 위의 4가지 개념과 같은방법이다
프래그먼트간의 데이터 공유를 위하여 액티비티에서 만들어주는경우
private val mainViewModel by activityViewModels<MainViewModel>()
부모의 액티비티에서 뷰모델만들어서 자식 프래그먼트들끼리 공유하는경우
by activityViewModels 키워드를 사용해 초기화 해준다 -> ViewModelOwner에 requireActivcity()넣어서 부모에서 만들어 주는것과 같은것이다.
이제 외부에서 인자가 들어가는경우를 볼것이다
외부에서 인자를 넣어줘야 하는 경우 (파라미터가 있는 경우)
1. ViewModelProvider.Factory
파라미터가 없던 ViewModelFactory의 연잔선상에서 Factory를 구현하면 파라미터를 소유하고 있는 ViewModel 객체의 인스턴스를 생성할 수 있다.
직접 구현한 Factory 클래스에 파라미터를 넘겨주어 create() 내에서 인스턴스를 생성할 때 활용하면 된다.
ViewModel
class HasParamViewModel(val param: String) : ViewModel()
ViewModelFactory
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()
}
}
}
Activity (or Fragment)
class MainActivity : AppCompatActivity() {
private lateinit var hasParamViewModel: HasParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val sampleParam = "Ready Story"
hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(sampleParam))
.get(HasParamViewModel::class.java)
}
}
이렇게해서 외부에서 뭐넣어주려면 팩토리 만들어서 사용하면된다.
그리고 번외로 context를 넣어줘야하는경우에는 다른 방법을 사용해야하는데 그거까지는 tooMuch인거 같아서
관련 링크만 달아놓겠다.
https://readystory.tistory.com/176
결국이렇게 사용처에서viewModel인스턴스를 만들어 원하는 변수들을뽑아와서 값을쓰던지 데이터 변화시키는 함수를 불러서 사용하면된다.
!!!
아그리고 뷰모델을 부모 액티비티에서 만들어서 각 자식 프래그먼트 사이의 정보공유 형식으로 사용할수도있다 그것도 개념만알고 사용은 적절히 하면될것이다.
궁금하면 링크에서 보자
그이후에 뷰모델 데이터 바인딩 라이브데이터와 연관해서 사용하는 예시를 보고싶다면 이블로그를 참고하자
https://hanyeop.tistory.com/168
변수같은것도 데이터 바인딩으로 연결하고 함수도 데이터 바인딩으로 연결해버림 이 예제에서
최종적으로 한마다 위의ViewModelFactory이런거 의존성주입해주면 필요없다는데 아직koin같은거 안해서 모르겠음 추후에 공부하면서 밝혀지겠지
-> 이해함 그냥 koin쓰면 get으로 알아서 다 찾아서 넣어버림 필요없음
프래그먼트간 뷰모델 공유하는것은 이블로그 글을 참고하자!!!
https://thdev.tech/androiddev/2020/07/13/Android-Fragment-ViewModel-Example/
실제로 스파크에서 프래그먼트간 뷰모델 공유할일이 있어서 사용해봤다
실제로 먼저 뷰모델을 만들고 서버통신하는 프래그먼트가 앞쪽에있었고
같은 프래그먼트를 액티비티뷰모델로 주입해서 사용했더니 그냥 그러한 정보들을 이미 변수에 저장되어있어
다음 프래그먼트에서 서버와 통신 하는 로직을 다 지워버릴수있었다.
-> 힐트 써져있어서 그냥 액티비티 뷰모델 그냥 ktx 사용해서 넣는것처럼 넣어버리면된다
'안드로이드' 카테고리의 다른 글
retrofit2 ,okhttp 적용했던거 koin 적용해서 리팩토링 해보자 (0) | 2021.11.23 |
---|---|
koin 기초 정리 (2) | 2021.11.03 |
Glide 사용시 필요한기능 찾아놓은것 (0) | 2021.11.01 |
DataBinding의 BindingAdapter (0) | 2021.11.01 |
databinding의 Two-way Binding (0) | 2021.11.01 |