해당 포스트는 https://newsletter.jorgecastillo.dev/p/using-jetpack-compose-with-mvvm 를 번역하였습니다.
몇 주 전에 사람들이 어떤 아키텍쳐 패턴에 익숙한지 알기 위해서 트위터에서 설문조사를 진행했습니다. 그러한 이유로 Compose + MVVM 으로 간단한 포스트를 작성하기로 했습니다.
First, the solution (첫번째 솔루션)
아래 코드는 NowInAndroid 에 존재하는 코드의 일부분 입니다. 실제 코드를 보고 싶으면 여기서 보실 수 있습니다
@Composable
fun BookmarksRoute(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: BookmarksViewModel = hiltViewModel(),
) {
val feedState by viewModel.feedUiState.collectAsStateWithLifecycle()
BookmarksScreen(
feedState = feedState,
removeFromBookmarks = viewModel::removeFromSavedResources,
onTopicClick = onTopicClick,
modifier = modifier,
)
}
class BookmarksViewModel @Inject constructor(
private val repo: UserDataRepository,
getSaveableNewsResources: GetUserNewsResourcesUseCase,
) : ViewModel() {
val feedUiState: StateFlow<NewsFeedUiState> =
getSaveableNewsResources()
.filterNot { it.isEmpty() }
.map { it.filter(UserNewsResource::isSaved) }
.map(NewsFeedUiState::Success)
.onStart { emit(Loading) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading,
)
fun removeFromSavedResources(newsResourceId: String) {
viewModelScope.launch {
repo.updateNewsResourceBookmark(newsResourceId, false)
}
}
}
Benefits of Compose + MVVM (Compose 와 MVVM 을 함께 사용할 때 장점들)
MVVM (Model-View-ViewModel) 과 같이 검증된 아키텍쳐와 컴포즈를 함께 사용하는 경우 여러가지 이점을 얻을 수 있습니다.
- 관심사의 분리 : UI 는 비즈니스 로직에서 분리되어 있어요. 따라서 비즈니스와 데이터 로직을 독립적으로 유닛테스트 할 수 있어요.
- 반응형 UI : 안드로이드 개발 업계의 최신 기술(SOTA : 예술의 경지) 인 반응형 데이터 타입을 컴포즈에 쉽게 통합시킬 수 있어요
- 익숙함 : 개발자들은 MVVM 에 매우 익숙하기 때문에, 새로운 동료의 온보딩하고 빠르게 반복 작업을 수행할 수 있어요.
Collecting state in a lifecyle-aware manner (생명주기 알고 있는 방식으로 값 수집하기)
Composable 함수내에서 값을 수집할 때(collect), 수집 행위가 생명주기를 확실히 알게 하는 것은 중요합니다. 이말인 즉슨 수집할 때 화면에 가시성(visibility) 에 따라 보여질 때 수집을 실행하고, 보여지지 않을 때 자동으로 중단될 수 있음을 의미합니다. 어플리케이션이 백그라운드로 이동했을 때와 같이 다양한 케이스에서 필요없는 리소스가 낭비되는 것을 방지해줍니다.
이를 위해 StateFlow 의 확장함수인 collectAsStateWithLifecycle 를 사용할 수 있어요. 해당 함수는 호스트의 lifecycle owner 가 특정 생명주기 상태보다 높거나 낮아지면 수집을 시작하고 취소하는 함수에요. (기본 값은 Lifecycle.State.STARTED 에요 내부에서 각각 생명주기에 대응되는 End 생명 주기를 매핑해주니 관련 코드를 봐도 좋아요) 해당 주제에 관심이 있다면 Consuming flows safely in JetPack Compose by Manuel Vivo 를 참고하세요.
해당 함수를 사용하는 예제를 볼까요 ? (NowInAndroid 에서 발췌했어요)
The ViewModel side (뷰모델에서 바라보기)
일반적인 안드로이드 앱에서 볼 수 있는 뷰모델 코드가 아래 있어요.
StateFlow 는 UI 보여지는 상태를 노출하는데 사용해요. Repository (혹은 Usecase)를 통해 전달되는 어떠한 데이터 흐름도 읽을 수 있고, 필요하다면 Flow 에서 제공하는 연산자를 활용해서 데이터를 변환하기도 해요. 여러개의 Flows 를 다룰 때는 Combine 같은 Flow 연산자를 이용해서 합쳐서 하나의 Flow 를 만들기도 해요.
목표는 하나의 흐름으로 만들어서 이를 staetIn 연산자를 사용해서 cold -> hot Flow 로 만들기 위함이에요.
참고 : 여러개의 Flow 를 하나로 병합하는 것은 하나의 UI state class 로 하나의 화면을 모두 표현하고 싶기 때문이에요. 대개 UI 가 수집할 수 있는 UI 상태는 단일 소스(single source) 로 관리하는 것이 좋아요.
stateIn 파라미터들을 봐볼까요 ? 첫번째 연산자에 viewModelScope 을 전달하면 ViewModel 이 클리어 될 때 Flow 도 자동으로 취소되어서, 이후에 Data Layer 에서 새로운 상태가 배출되거나, 백그라운드 작업 중일 때 메모리 릭을 회피하는데 도움을 줍니다.
두번째 파라미터인 WhileSubscribed(5_000) 은 다소 논란의 여지가 있는 트릭이에요. rotations 과 같은 구성 요소 변경이 발생했을 때(보통 5초 내로 완료되어요) 수집이 취소 되는 것을 방지해줘요. WhileSubscribed 는 StateFlow 에 첫번째 구독자가 나타날 때 데이터 공유를 시작해요. 그리고 마지막 구독자가 사라지만 즉시 수집을 중단해요. replay cache 에 존재하는 데이터는 계속 유지 됩니다.
마지막으로 initialValue 에는 배출할 초기 상태가 전달 됩니다. (여기서는 Loading 이네요)
이정도만 하면 요구조건 완료입니다. 대부분 모든 안드로이드 앱을 이와 같은 방식으로 표현할 수 있습니다.
Unidirectional data flow (단방향 데이터 흐름)
대부분의 아키텍쳐, 아키텍쳐 패턴은 UI 에 관찰 가능한 상태를 단방향 데이터 흐름(UDF)으로 노출하고 있습니다. MVVM 으로 UDF 를 구현하는 것은 간단합니다.
- Repository (or Usecase) 에서 데이터를 읽는 모든 액션을 Flow 로 반환 합니다.
- 뷰모델 또는 UseCase 는 SSOT(단일 진실 공급원) 과 같은 데이터베이스에서 항상 읽도록 처리합니다. 그래서 어플리케이션 데이터에 변화가 발생하면, Flow 연산자를 사용해서 하나의 상태로 만들고(Reduce), 원하는 형태로 변환(transformed) 해 UI state 를 만듭니다.
- Data Layer 에서 발생하는 일회성 작업(네트워크 요청 등)은 suspend 함수로 처리 합니다(위 BookMarksViewModel 클래스의 removeFromSavedResources 를 의미해요)
이를 통해 데이터가 한 방향으로만 전달되도록 만들 수 있습니다. ♻️
- 유저는 버튼을 클릭합니다.
- UI 에서 ViewModel 로 유저 상호작용에 대해서 통지합니다.
- ViewModel 은 코루틴 함수를 실행해서 suspend 함수를 이용해 네트워크 요청등을 처리합니다.
- 네트워크 요청이 완료되면 데이터베이스에(꼭 데이터 베이스 일 필요는 없습니다 SSOT 만 지켜주면 됩니다) 데이터를 저장합니다.
- ViewModel 이 데이터베이스에서 변경이 일어난 것을 관찰해서 새로운 데이터를 방출합니다.
- ViewModel 에서 필요한 UI state 로 값을 줄여내고(reduce) 변환(transform) 합니다.
- UI 에서 새로운 값을 수집하고 그려냅니다.
Conclusion (결론)
Compose 의 선언적인 특성은 관찰 가능한 SSOT 의 형태로 UI state 로 노출하는 모든 아키텍쳐 패턴과 꽤나 조합이 잘 맞습니다. 또한 컴포즈는 새로운 UI state 가 방출되면, 매번 모든 UI 트리를 그리는 것이 아니라, 이미 만들어진 UI 트리에서 변경된 부분을 효율적으로 변경해서 사용합니다. 이부분이 안드로이드 뷰 시스템과 비교했을 때 큰 이점입니다.
참고 : 글에서는 지렛대(레버리지) 효과라고 표현하네요.
그럼 20000-
'Android > 번역' 카테고리의 다른 글
[번역] Modifiers: order of precedence (0) | 2023.12.30 |
---|---|
[번역] The color of Composable functions (1) | 2023.12.30 |
[번역] Crash Course on the Android UI Layer Part 2 (2) | 2023.12.29 |
[번역] Crash Course on the Android UI Layer Part 1 (1) | 2023.12.28 |
LiveData, Coroutine, Flow 를 이용한 반응형 UI - Part 3 (0) | 2021.06.07 |