이 기사의 목표는 Model-View-ViewModel 아키텍처 패턴이 GUI 아키텍처의 표시 논리와 관련하여 일부 상황에서 매우 어색한 관심사 분리를 나타내는 이유를 설명하는 것입니다.
우리는 MVVM의 두 가지 변형을 살펴볼 것입니다. 단 한 가지 방법) 및 프로젝트 요구 사항에 따라 한 변형을 다른 변형보다 선호하는 이유.
MVVM 대 MVP/MVC?
라이브 일요일 Q&A 세션에서 가장 많이 받는 질문은 다음과 같습니다.
MVVM 대 MVP/MVC?
이 질문을 받을 때마다 저는 단일 GUI 아키텍처가 모든 상황에서 훌륭하게 작동하지 않는다는 아이디어를 재빨리 강조합니다.
왜, 당신은 물을 수 있습니까? 주어진 애플리케이션에 대한 최상의 아키텍처(또는 최소한 좋은 선택)는 당면한 요구 사항에 크게 의존합니다.
이 단어의 요구사항에 대해 간단히 생각해 보겠습니다. 실제로 의미:
- UI가 얼마나 복잡합니까? 간단한 UI는 일반적으로 이를 조정하는 데 복잡한 논리가 필요하지 않지만 복잡한 UI는 원활하게 작동하기 위해 광범위한 논리와 세분화된 제어가 필요할 수 있습니다.
- 테스트에 얼마나 관심이 있습니까? 일반적으로 프레임워크 및 OS(특히 사용자 인터페이스 ) 테스트하려면 추가 작업이 필요합니다.
- 재사용성과 추상화를 어느 정도 촉진하고 싶습니까? 다양한 플랫폼에서 애플리케이션의 백엔드, 도메인 및 프레젠테이션 로직을 공유하려면 어떻게 해야 할까요?
- 본능적으로 실용적입니까? , 완벽주의자 , 게으름 , 또는 다른 시간에 다른 상황에서 위의 모든 항목을 사용하시겠습니까?
위에 나열된 요구 사항 및 우려 사항과 관련하여 MVVM이 어떻게 작동하는지 자세히 논의하는 기사를 작성하고 싶습니다. 불행히도 여러분 중 일부는 MVVM을 수행하는 방법이 한 가지뿐이라고 잘못 생각하고 있을 것입니다.
대신에 MVVM의 일반적인 개념에 대해 매우 뚜렷한 장점과 단점을 나타내는 두 가지 다른 접근 방식에 대해 논의할 것입니다. 하지만 먼저 일반적인 아이디어부터 시작하겠습니다.
보기 클래스를 참조하지 마십시오
고대 영어를 읽을 수 없는 내 친구들을 위해: “보기 클래스를 참조할 수 없습니다. ."
ViewModel이라는 이름을 사용하는 것 외에도(클래스가 논리로 가득 차면 혼란스럽습니다. ), MVVM 아키텍처의 철칙 중 하나는 ViewModel에서 View를 참조할 수 없다는 것입니다.
이제 혼동의 첫 번째 영역은 "참조"라는 단어에서 발생할 수 있습니다. 여러 수준의 전문 용어를 사용하여 다시 설명하겠습니다.
- ViewModel은 View에 대한 참조(멤버 변수, 속성, 변경 가능/불변 필드)를 소유할 수 없습니다.
- ViewModel은 View에 의존하지 않을 수 있습니다.
- ViewModel이 View와 직접 통신하지 못할 수 있습니다.
이제 Android 플랫폼에서 이 규칙의 이유는 소프트웨어 아키텍처에 대해 아는 것 같은 누군가가 나쁘다고 해서 단순히 그것을 깨는 것이 나쁘다는 것이 아닙니다.
아키텍처 구성요소의 ViewModel 클래스를 사용할 때(인스턴스가 지속하도록 설계되었습니다. 적절한 경우 Fragment/Activity 수명 주기보다 깁니다. ), 보기를 참조하면 심각한 메모리 누수가 필요합니다. .
일반적으로 MVVM이 이러한 참조를 허용하지 않는 이유는 가상적으로 목표입니다. View와 ViewModel을 더 쉽게 테스트하고 작성할 수 있습니다.
다른 사람들은 ViewModels의 재사용성을 촉진한다고 지적할 수도 있지만 이것이 이 패턴에서 문제가 발생하는 부분입니다. .
코드를 보기 전에 개인적으로 LiveData를 사용하지 않습니다. 내 자신의 생산 코드에서. 요즘에는 저만의 게시자-구독자 패턴을 작성하는 것을 선호하지만 아래에서 말한 내용은 ViewModel에서 View로의 PubSub/Observer Pattern 링크를 허용하는 모든 라이브러리에 적용됩니다.
이 기사에는 다음과 같은 여러 동일한 아이디어를 다루는 비디오 자습서가 함께 제공됩니다.
ViewLogic + ViewModel 또는 View + ViewModelController?
내가 이전 섹션에서 "파괴"라고 말한 것은 패턴이 문자 그대로 부서진다는 것을 말하는 것이 아닙니다. 내 말은 그것이 (적어도) 매우 뚜렷한 모양, 이점 및 결과를 갖는 두 가지 다른 접근 방식으로 나뉩니다.
이 두 가지 접근 방식을 고려하고 둘 중 하나를 선호하는 경우
첫 번째 접근 방식:재사용 가능한 ViewModel의 우선 순위 지정
내가 말할 수 있는 한, MVVM을 구현하는 대부분의 사람들은 n 동안 재사용될 수 있도록 ViewModel의 재사용성을 촉진하는 것을 목표로 삼습니다. 다른 보기의 수(다대일 비율).
간단히 말해서 이 재사용성을 달성할 수 있는 두 가지 방법이 있습니다.
- 특정 보기를 참조하지 않음. 현시점에서 이것이 뉴스가 되지 않기를 바랍니다.
- 알아서 UI 의 세부정보를 최대한 적게 일반적으로
두 번째 요점은 모호하거나 반직관적으로 들릴 수 있으므로(참조하지 않는 항목에 대해 어떻게 알 수 있습니까?), 이제 몇 가지 코드를 살펴볼 때라고 생각합니다.
class NoteViewModel(val repo: NoteRepo): ViewModel(){
//Note: you may also publish data to the View via Databinding, RxJava Observables, and other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with AAC
val noteState: MutableLiveData<Note>()
//...
fun handleEvent(event: NoteEvent) {
when (event) {
is NoteEvent.OnStart -> getNote(event.noteId)
//...
}
}
private fun getNote(noteId: String){
noteState.value = repo.getNote(noteId)
}
}
이것은 매우 단순화된 예이지만 요점은 이 특정 ViewModel이 공개적으로 노출하는 유일한 것(handleEvent 함수 제외)은 단순한 Note 객체라는 것입니다.
data class Note(val creationDate:String,
val contents:String,
val imageUrl: String,
val creator: User?)
이 특정 접근 방식을 사용하면 ViewModel이 특정 View뿐만 아니라 세부 정보와 확장하여 프레젠테이션 로직과 잘 분리됩니다. 특정 보기의.
내가 말하는 것이 여전히 모호한 경우 다른 접근 방식을 설명하면 명확해질 것이라고 약속합니다.
이전 제목은 "ViewLogic + ViewModel... "는 사용하거나 진지하게 받아들일 의도가 아닙니다. 즉, 매우 분리되고 재사용 가능한 ViewModel을 사용함으로써 우리는 이제 View 자체에 의존하여 이 Note 개체를 화면에 렌더링/바인딩하는 방법을 알아내는 작업을 수행하게 되었습니다.
우리 중 일부는 View 클래스를 Logic으로 채우는 것을 좋아하지 않습니다.
여기에서 상황이 매우 흐릿해지고 프로젝트 요구 사항에 따라 달라집니다. . View 클래스를 다음과 같은 논리로 채우는 것이 아닙니다.:
private fun observeViewModel() {
viewModel.notes.observe(
viewLifecycleOwner,
Observer { notes: List<Note> ->
if (notes.isEmpty()) showEmptyState()
else showNoteList(notes)
}
)
//..
}
...항상 나쁜 일이지만 플랫폼에 밀접하게 결합된 클래스(예:Fragments)는 테스트하기 어렵고 논리가 포함된 클래스가 테스트할 가장 중요한 클래스입니다!
한마디로 내가 생각하는 좋은 아키텍처의 황금 원칙인 관심 분리를 적용하지 않는 것입니다. .
내 개인적인 의견은 매우 높은 수준으로 관심 분리를 적용할 가치가 있다는 것입니다. 그러나 그것이 의미하는 바에 대해 가장 희미한 실마리가 없는 사람들에 의해 작성된 많은 현금소 신청서가 있다는 사실을 실수하지 마십시오.
어쨌든 고유한 부작용이 있는 접근 방식은 다음에 논의할 것입니다. , 다시 한 번 보기에서 프레젠테이션 논리를 제거합니다.
어쨌든 대부분이 그렇습니다.
두 번째 접근 방식:Humble View, Control-Freak ViewModel
때때로 View를 세밀하게 제어하지 못하는 경우가 있습니다(ViewModel의 재사용성을 우선시한 결과). 실제로는 정말 짜증납니다.
이전 접근 방식을 무분별하게 적용하는 것에 대한 열의를 덜기 위해 나는 자주 안함 ViewModel을 재사용해야 함 .
아이러니하게도 "너무 많은 추상화"는 MVVM보다 MVP에 대한 일반적인 비판입니다.
즉, View에 대한 이러한 세분화된 제어를 다시 얻기 위해 ViewModel에 참조를 다시 추가할 수는 없습니다. 기본적으로 MVP + 메모리 누수입니다(AAC에서 ViewModel을 계속 사용하고 있다고 가정).
그렇다면 대안은 거의 모든 동작을 포함하도록 ViewModel을 빌드하는 것입니다. , 상태 및 프레젠테이션 로직 주어진 보기의. 물론 View는 여전히 ViewModel에 바인딩되어야 하지만 View에 대한 충분한 세부 정보가 ViewModel에 있으므로 View의 기능이 하나의 라이너로 축소됩니다(작은 예외 제외).
Martin Fowler의 명명 규칙에서는 이를 수동 보기/화면이라고 합니다. 이 접근 방식에 더 일반적으로 적용할 수 있는 이름은 Humble Object Pattern입니다. .
이를 달성하려면 View에 있는 모든 컨트롤 또는 위젯에 대해 ViewModel이 관찰 가능한 필드(그러나 이를 달성하는 경우 – 데이터 바인딩, Rx, LiveData 등)를 보유해야 합니다.
class UserViewModel(
val repo: IUserRepository,
){
//The actual data model is kept private to avoid unwanted tampering
private val userState = MutableLiveData<User>()
//Control Logic
internal val authAttemptState = MutableLiveData<Unit>()
internal val startAnimation = MutableLiveData<Unit>()
//UI Binding
internal val signInStatusText = MutableLiveData<String>()
internal val authButtonText = MutableLiveData<String>()
internal val satelliteDrawable = MutableLiveData<String>()
private fun showErrorState() {
signInStatusText.value = LOGIN_ERROR
authButtonText.value = SIGN_IN
satelliteDrawable.value = ANTENNA_EMPTY
}
//...
}
결과적으로 View는 여전히 ViewModel에 연결해야 하지만 그렇게 하는 데 필요한 함수는 작성하기가 매우 간단해집니다.
class LoginView : Fragment() {
private lateinit var viewModel: UserViewModel
//...
//Create and bind to ViewModel
override fun onStart() {
super.onStart()
viewModel = ViewModelProviders.of(
//...
).get(UserViewModel::class.java)
//start background anim
(root_fragment_login.background as AnimationDrawable).startWithFade()
setUpClickListeners()
observeViewModel()
viewModel.handleEvent(LoginEvent.OnStart)
}
private fun setUpClickListeners() {
//...
}
private fun observeViewModel() {
viewModel.signInStatusText.observe(
viewLifecycleOwner,
Observer {
//"it" is the value of the MutableLiveData object, which is inferred to be a String automatically
lbl_login_status_display.text = it
}
)
viewModel.authButtonText.observe(
viewLifecycleOwner,
Observer {
btn_auth_attempt.text = it
}
)
viewModel.startAnimation.observe(
viewLifecycleOwner,
Observer {
imv_antenna_animation.setImageResource(
resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName)
)
(imv_antenna_animation.drawable as AnimationDrawable).start()
}
)
viewModel.authAttemptState.observe(
viewLifecycleOwner,
Observer { startSignInFlow() }
)
viewModel.satelliteDrawable.observe(
viewLifecycleOwner,
Observer {
imv_antenna_animation.setImageResource(
resources.getIdentifier(it, "drawable", activity?.packageName)
)
}
)
}
이 예제의 전체 코드는 여기에서 찾을 수 있습니다.
아마 눈치채셨겠지만 우리는 이 ViewModel을 다른 곳에서 재사용하지 않을 것입니다. . 또한 우리의 View는 (코드 적용에 대한 표준과 선호도에 따라) 충분히 겸손해졌으며 작성하기 매우 쉽습니다.
때때로 프레젠테이션 로직의 분포 사이에서 일종의 반값을 찾아야 하는 상황에 직면할 것입니다. 이러한 접근 방식 중 하나를 엄격하게 따르지 않는 View와 ViewModel 사이.
저는 한 접근 방식을 다른 접근 방식보다 옹호하는 것이 아니라 당면한 요구 사항에 따라 접근 방식을 유연하게 조정하도록 권장합니다.
기본 설정 및 요구 사항에 따라 아키텍처 선택
이 기사의 요점은 개발자가 Android 플랫폼에서 MVVM 스타일 GUI 아키텍처를 구성할 때 취할 수 있는 두 가지 접근 방식을 살펴보는 것이었습니다(일부는 다른 플랫폼으로 이월됨).
사실 이 두 가지 접근 방식 내에서도 작은 차이에 대해 더 구체적으로 알 수 있습니다.
- View가 소유한 모든 개별 위젯/컨트롤에 대한 필드를 관찰해야 하거나 단일 모델 을 게시하는 하나의 필드를 관찰해야 합니다. 전체 보기를 매번 새로 렌더링하시겠습니까?
- Presenter나 Controller와 같은 것을 믹스에 추가함으로써 ViewModel을 일대일로 만들지 않고 Humble Objects로 View를 유지할 수 있을까요?
Talk는 저렴하며 코드에서 이러한 내용을 시도하고 배우기를 강력히 권장합니다. 나 같은 사람에게 어떻게 해야 하는지에 의존할 필요가 없도록 하십시오.
궁극적으로 훌륭한 아키텍처를 구성하는 두 가지 요소는 다음과 같은 고려 사항으로 귀결된다고 생각합니다.
먼저 선호하는 방법을 찾을 때까지 여러 가지 접근 방식을 시도해 보세요. . 각 스타일에서 실제로 애플리케이션을 빌드하고(간단할 수 있음) 느낌이 무엇인지 확인하는 것이 가장 좋습니다. .
둘째, 선호도는 제쳐두고 다양한 스타일이 서로 다른 결핍에 대한 대가로 서로 다른 이점을 강조하는 경향이 있음을 이해하십시오. 결국 맹신이 아닌 프로젝트 요구 사항에 대한 이해를 바탕으로 좋은 선택을 할 수 있을 것입니다. .
소프트웨어 아키텍처에 대해 자세히 알아보기:
소셜
https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
https://wiseassblog.com/