시배's Android
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (2) 본문
Mavericks
매버릭스(Mavericks)는 Airbnb, Tonal 및 기타 대형 앱에서 사용되는 안드로이드 MVI 프레임워크로, 쉽게 배울 수 있으면서도 복잡한 플로우를 지원할만큼 강력합니다.
매버릭스를 만들 때 목표는 제품을 더 쉽고 빠르게, 더 재미있게 개발하는 것이었습니다. 우리는 매버릭스가 성공하기 위해 안드로이드 개발에 처음으로 참여하는 사람들에게도 쉽게 배울 수 있어야 하지만 Airbnb에서 가장 복잡한 화면도 지원할 수 있어야 한다고 믿습니다.
매버릭스는 Airbnb의 수백 개의 화면에서 사용되며, 새로운 화면의 100%에서 사용되고 있습니다. 또한 10억 개 이상의 다운로드를 가진 앱부터 작은 샘플 앱까지 무수히 많은 다른 앱에서도 채택되고 있습니다.
매버릭스는 Android Jetpack과 Kotlin Coroutines 위에 구축되어서 구글의 표준 라이브러리를 보완하는 형태로 생각할 수 있습니다. 따라서 기존의 기술 스택을 버리는 대신 보완적으로 도입됩니다.
다음은 간단한 화면의 예시입니다. 더 복잡한 화면은 더 많은 상태 속성이나 ViewModel 함수를 가질 수 있지만, 디버깅이나 코드 읽기도 크게 복잡해지지 않습니다.
/** State classes contain all of the data you need to render a screen. */
data class CounterState(val count: Int = 0) : MavericksState
/** ViewModels are where all of your business logic lives. It has a simple lifecycle and is easy to test. */
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {
fun incrementCount() = setState { copy(count = count + 1) }
}
/**
* Fragments in Mavericks are simple and rarely do more than bind your state to views.
* Mavericks works well with Fragments but you can use it with whatever view architecture you use.
*/
class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {
private val viewModel: CounterViewModel by fragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
counterText.setOnClickListener {
viewModel.incrementCount()
}
}
override fun invalidate() = withState(viewModel) { state ->
counterText.text = "Count: ${state.count}"
}
}
MavericksState
매버릭스(Mavericks) 화면을 생성하는 첫 번째 단계는 상태(State)의 함수로서 모델링하는 것입니다. 매버릭스 상태 인터페이스(MavericksState) 자체로는 아무 동작을 하지 않지만, 해당 클래스가 상태로 사용될 의도를 나타냅니다.
화면을 상태의 함수로서 모델링하는 것은 다음과 같은 이점이 있습니다:
- 스레드 안전(Thread safe)합니다.
- 여러분과 다른 엔지니어들이 쉽게 이해할 수 있습니다.
- 해당 화면이 도달하기까지의 이벤트 발생 순서에 독립적으로 동일하게 렌더링됩니다.
- 어떤 유형의 화면도 렌더링하는 데 강력합니다.
- 쉽게 테스트할 수 있습니다.
매버릭스는 또한 상태 클래스가 다음과 같은 조건을 충족하도록 강제합니다:
- Kotlin 데이터 클래스여야 합니다.
- 오직 불변 속성만을 사용해야 합니다.
- 모든 속성에는 기본 값이 있어야 하며, 이는 화면이 즉시 렌더링될 수 있도록 합니다.
매버릭스는 이러한 규칙들을 디버그 체크를 통해 강제합니다.
이 개념은 화면에 대한 추론과 테스트를 매우 쉽게 만들어 줍니다. 상태 클래스가 주어진다면 해당 화면이 올바르게 보이리라는 높은 확신을 가질 수 있습니다. 예시를 드리겠습니다.
data class UserState(
val score: Int = 0,
val previousHighScore: Int = 150,
val livesLeft: Int = 99,
) : MavericksState
Derived Properties
상태(State)는 일반적인 Kotlin 데이터 클래스이기 때문에 다음과 같이 특정 상태 조건을 나타내는 파생 속성(derived properties)을 생성할 수 있습니다.
data class UserState(
val score: Int = 0,
val previousHighScore: Int = 150,
val livesLeft: Int = 99,
) : MavericksState {
// Properties inside the body of your state class are "derived".
val pointsUntilHighScore = (previousHighScore - score).coerceAtLeast(0)
val isHighScore = score >= previousHighScore
}
pointsUntilHighScore나 isHighScore와 같이 파생된 속성으로 로직을 배치하는 것은 다음과 같은 이점이 있습니다:
- onEach를 사용하여 이러한 파생 속성의 변경에 대해 구독할 수 있습니다.
- 파생 속성은 항상 기본 상태와 일치하므로 동기화 문제가 발생하지 않습니다.
- 상태를 추론하기가 매우 쉽습니다.
- 이러한 파생 속성에 대한 단위 테스트를 작성하기가 매우 쉽습니다.
MavericksViewModel
ViewModel은 다음과 같은 책임을 가지고 있습니다:
1. 상태(State)의 업데이트:
- ViewModel은 상태를 업데이트하는 역할을 담당합니다.
- setState { copy(yourProp = newValue) }와 같은 문법을 사용하여 ViewModel 내부에서 상태를 업데이트할 수 있습니다.
- 이 문법에서 람다(lambda)의 시그니처는 S.() -> S로, 람다의 수신자(Receiver, 즉 this)는 람다가 호출될 때의 현재 상태를 가리킵니다. 람다는 새로운 상태를 반환합니다.
- 상태 클래스가 Kotlin 데이터 클래스이기 때문에 copy 함수를 사용하여 새로운 상태 객체를 생성할 수 있습니다.
- 람다는 동기적으로 실행되지 않으며, 백그라운드 스레드에서 대기열에 추가되어 실행됩니다. 스레딩에 대한 자세한 내용은 문서를 참조하시기 바랍니다.
2. 상태 변경을 구독하기:
- ViewModel에서 상태 변경을 구독할 수 있습니다.
- 이를 통해 분석(analytics) 등의 목적으로 사용할 수 있습니다.
- 일반적으로 이러한 구독은 init { ... } 블록에서 수행됩니다.
Mavericks의 ViewModel은 개념적으로는 Jetpack ViewModel과 거의 동일하며, MavericksState 클래스에 대한 제네릭(Generic) 타입을 추가함으로써 구분됩니다.
비동기/데이터베이스/네트워크 작업 처리에 대해서는 Mavericks에서는 이를 쉽게 다룰 수 있도록 Async<T>와 execute(...)와 같은 도구들을 제공합니다.
// Invoked every time state changes
onEach { state ->
}
// Invoked whenever propA changes only.
onEach(YourState::propA) { a ->
}
// Invoked whenever propA, propB, or propC changes only.
onEach(YourState::propA, YourState::propB, YourState::propC) { a, b, c ->
}
stateFlow
MavericksViewModel은 stateFlow 속성을 노출하는데, 이는 일반적인 Kotlin Flow입니다. stateFlow는 현재 상태와 이후 업데이트를 발행하며, 원하는 대로 사용할 수 있습니다. 위에서 설명한 onEach와 같은 도우미 함수들은 자동적으로 라이프사이클에 맞춰서 취소되는 래퍼(wrapper)입니다.
한 번만 상태에 접근하기
만약 상태의 값을 한 번만 가져오고자 한다면, withState { state -> ... }를 사용할 수 있습니다.
ViewModel 내부에서 호출될 때, 이는 동기적으로 실행되지 않습니다. 대신 백그라운드 큐에 넣어져서 모든 대기 중인 setState 리듀서들이 withState 호출 이전에 호출되도록 보장됩니다.
MavericksView
MavericksView는 실제로 상태 클래스를 화면에 렌더링하는 곳입니다. 대부분의 경우 이는 Fragment가 될 것이지만, 꼭 그럴 필요는 없습니다. MavericksView를 구현함으로써 다음과 같은 기능들을 이용할 수 있습니다.
1. 뷰 모델 접근과 업데이트:
- 어떤 뷰 모델 델리게이트를 통해 MavericksViewModel에 접근할 수 있습니다. 이렇게 하면 자동으로 상태 변경을 구독하고 invalidate() 함수를 호출합니다.
- invalidate() 함수를 오버라이드합니다. 이 함수는 위에서 언급한 델리게이트를 통해 접근한 모든 뷰 모델의 상태가 변경될 때마다 호출됩니다. invalidate() 함수는 각 상태 변경마다 UI를 다시 그리는 데 사용됩니다.
2. 뷰 모델 델리게이트:
- activityViewModel(): 뷰 모델을 Activity에 지정합니다. Activity 내에서 이와 같은 유형의 뷰 모델을 요청하는 모든 Fragment는 동일한 인스턴스를 받게 됩니다. 화면 간 데이터 공유에 유용합니다.
- fragmentViewModel(): 뷰 모델을 Fragment에 지정합니다. 자식 Fragment에서도 접근할 수 있지만, 부모나 형제 Fragment는 다른 인스턴스를 받게 됩니다.
- parentFragmentViewModel(): 원하는 유형의 뷰 모델을 가진 상위 Fragment로 올라가며 찾습니다. 만약 발견되지 않으면, 가장 최상위 상위 Fragment에 대해 자동으로 새로운 인스턴스를 생성합니다.
- existingViewModel(): activityViewModel()과 동일하지만, 다른 누군가에 의해 이미 뷰 모델이 생성되지 않았을 경우 예외를 throw합니다.
- navGraphViewModel(navGraphId: Int): 해당 ID를 가진 Jetpack Navigation 그래프에 뷰 모델을 지정합니다. 이 기능은 mvrx-navigation 아티팩트가 필요합니다.
3. 수동으로 상태 구독:
- 대부분의 경우 invalidate() 함수를 오버라이드하고 뷰를 업데이트하는 것으로 충분합니다. 하지만 애니메이션 시작과 같은 상태 구독이 필요한 경우, ViewModel에서 onEach 구독 중 하나를 호출할 수 있습니다. 뷰가 Fragment인 경우, 이러한 구독은 onCreate()에서 설정되어야 합니다.
4. 상태 값 한 번에 접근하기:
- 상태의 값을 한 번만 가져오고자 한다면, withState { state -> ... }를 사용할 수 있습니다.
- 뷰 모델 외부에서 호출될 때, 이는 동기적으로 실행됩니다.
5. 상태 변경 트리거하기:
- 뷰 모델은 뷰에서 호출할 수 있는 이름이 지정된 함수들을 노출해야 합니다. 예를 들어, 카운터 뷰 모델은 incrementCount()와 같은 함수를 노출하여 뷰에서 접근할 수 있는 명확한 API를 제공할 수 있습니다.
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (1)
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (1)
안드로이드 앱 개발에서 MVI (Model-View-Intent) 아키텍처는 현재 많은 개발자들에게 인기가 높아지고 있는 패턴입니다. MVI는 앱의 상태 관리와 UI 처리를 효과적으로 분리하여 앱의 유지보수성을 향
si8ae.tistory.com
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (3)
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (3)
Mavericks + Compose + Naver API Android 앱 개발에서 최신 기술인 Mavericks와 Compose를 활용하여 Naver API를 사용하여 책을 검색하는 앱을 구현해보겠습니다. MavericksState data class SearchUiState( val keyword: String = "",
si8ae.tistory.com
https://airbnb.io/mavericks/#/README
Mavericks Docs
airbnb.io
'Android > Design Patterns' 카테고리의 다른 글
Design Patterns | Orbit로 MVI 패턴 찍먹해보기 (2) (0) | 2023.08.08 |
---|---|
Design Patterns | Orbit로 MVI 찍먹 해보기 (1) (0) | 2023.08.01 |
Design Patterns | Top Android MVI libraries in 2021 번역 (1) | 2023.07.27 |
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (3) (0) | 2023.07.24 |
Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (1) (0) | 2023.07.23 |