코틀린 뷰모델에서 올바른 UI 액션 핸들링 방법
여러개의 화면에서 동일한 UI컴포넌트를 사용해야 했던적이 있나요?
각 화면마다 ViewModel이 따로 있어서 UI 상호작용을 반복해서 처리해야했던 경험이 있었나요?
그렇다면 이 블로그글이 바로 여러분을 위한 것입니다.
소개
안드로이드 개발자라면 종종 동일한 UI 기능을 여러 ViewModel에 걸쳐 구현해야 하는 상황을 마주하게 됩니다.
예를들어 우리는 게시물 표시, 댓글을 작성하거나, 사용자 상호작용을 처리하는 것처럼 비슷한 기능을 가진 여러개의 화면을 갖고 있을 수 있습니다.
이러한 UI상호작용을 각각의 뷰모델에서 따로 처리하다보면 금방 코드가 지저분해지고 중복도 심해질 수 있습니다.
앱이 커지고 화면수가 많아질수록 이 이슈는 더 심각해지고, 결국 유지보수하기 어려운 코드베이스와 확장성 문제로 이어지게 됩니다.
이 코드 중복 문제는 몇몇의 잘 알려진 해결책이 있습니다. 일반적인 안드로이드 개발자들이 사용하는 방법입니다.
- 헬퍼 클래스 접근 방법
- 추상화 또는 delegate 사용 방법
이 섹션에서는 우리는 이 해결책들을 자세히 살펴보며, 각각의 접근 방식이 코드 중복문제를 어떻게 해결하는지와 그 한계점들을 짚어보겠습니다.
그 후 저만에 해결책을 소개하겠습니다. 이러한 아이디어들을 기반으로 하여 한계들을 극복하고 UI 상호작용을 더욱 효율적으로 관리할 수 있는 방법을요.
이 해결책은 이미 Medials 앱에 코드베이스에서 사용해본 방식으로, UI상호작용 처리 코드를 거의 플러그앤 플레이처럼 간편하게 만들어줍니다.
제 해결책은 BaseViewModels을 사용하라고 제안하거나 그것을 기반으로 하지않습니다.
여기서 BaseViewModel은 단지 예시일 뿐이며,
우리가 어떤 클래스나 인터페이스로부터 기능을 상속받았는지만 중요합니다.
그게 BaseViewModel, ViewModel Decompose에 ComponentContext든 상관없습니다.
뷰모델에서 UI 상호작용을 처리하기 위해 공유하는 방법
여러 화면에 걸쳐 동일한 UI 컴포넌트를 표시할 때, 각각의 뷰모델에서 그 컴포넌트의 상호작용을 처리하면 코드의 반복과 지저분한 로직으로 이어집니다.
우리는 이 문제를 해결하기위해 3가지 접근방법을 설명하겠습니다.
1. Helper 클래스 접근방법
- 간단하지만 어떤 동작도 재정의할 수 없습니다.
- 뷰모델의 직접적인 기능은 아닙니다.
2. 코틀린 delegate를 통한 구성
- 디자인에 좋고 동작 재정의를 지원합니다
- viewmodelscope나 다른 뷰모델 기능에 접근할 수 없습니다.
- 다른 클래스를 상속받을 수 없습니다.
3. 내 해결방법 - 기본 함수들을 가진 인터페이스
- 깔끔하고 재 사용 가능하고, 동작 재정의를 지원합니다
- viewmodel에 기능에 전부 접근가능하고 다른 모든 상속 클래스에 접근할 수 있습니다.
- 인터페이스로부터 단지 구현만하면 어떤 viewmodel도 쉽게 끼워넣을 수 있습니다.
이 블로그에서는 모든 솔루션을 소개하고, 샘플프로젝트에서 구현된 걸 확인하실 수 있습니다.
셋업
게시물 UI 요소가 있다고 가정해보겠습니다. 여기에 MVI 패턴에서 제안하는 것처럼 일부 UI 상호작용이 sealed interface로 표현되어있다고 가정해보겠습니다.
sealed interface PostAction {
data class Clicked(val id: String) : PostAction
data class LikeClicked(val id: String) : PostAction
data class ShareClicked(val id: String) : PostAction
}
그리고 BaseViewModel 클래스에서 매 뷰모델에서 필요한 공통 기능을 담기 위해 사용하는 클래스입니다.
abstract class BaseViewModel : ViewModel() {
var showShackBar by mutableStateOf("")
var showBottomSheet by mutableStateOf("")
fun navigate() {
//Implementation
}
fun showSnackbar(message: String) {
showSnackBar = message
}
}
추가적인 헬퍼 클래스 접근 방식
이 접근방식은, 분리된 헬퍼 클래스를 생성해 UI 상호작용 처리를 캡슐화 합니다. 우리의 뷰모델에서 이 클래스는 프로퍼티들을 유지합니다.
첫번째로, 우리는 헬퍼클래스인 PostActionHandler 를 명시합니다. 이것은 상호작용을 처리합니다.
class PostActionHandler(private val viewModel: BaseViewModel) {
fun handleAction(action: PostAction) = when (action) {
is PostAction.Clicked -> viewModel.navigate()
is PostAction.LikeClicked -> viewModel.showSnackBar("Liked")
is PostAction.ShareClicked -> { /*Implementation */ }
}
}
이제 PostScreenViewModel 에서 우리는 PostActionHandler 객체를 생성합니다.
class PostScreenViewModel : BaseViewModel() {
val actionHandler = PostActionHandler(this)
}
@Composable
fun PostScreen(viewModel: PostScreenViewModel) {
PostItem(onAction = viewModel.actionHandler::handleAction)
}
@Composable
fun PostItem(onAction: (PostAction) -> Unit) {
}
이 접근 방식은 여러 뷰모델에 중복되는 코드를 해결해주지만, 몇 가지 큰 단점이 있습니다.
이 단점은
- 커스터마이징의 한계 : 캡슐화된 PostActionHandler 로부터 어떤 행동도 재정의 할 수 없습니다.
- 프로퍼티를 통한 접근: 기능을 뷰모델 자체에 추가하기 보다는 우리는 단지 뷰모델의 프로퍼티로 추가하고 있을 뿐입니다.
이 접근방식이 UI 상호 작용 처리를 위한 방법으로 권장되지 않는 이유는 동작을 오버라이드 할 수 있는 가능성이 없기 떄문입니다.
https://medium.com/proandroiddev/handling-ui-actions-the-right-way-in-kotlin-viewmodels-119a06bb43ef
기타
encounter 마주치다
messy 복잡하고 정돈되지 않은
significant 상당한
repetition 반복
cluttered logic 지저분한 로직
drawbacks 단점
rather than ~하는 대신에