반응형
Orbit 없이 MVI 패턴 적용기
사이드 프로젝트를 진행하면서 단방향 데이터 흐름을 구현하기 위해 sealed class를 사용하다 보니 nav에서 back을 할 경우 이전 viewmodel에 존재하는 상태 값 때문에 자동으로 navigation 되는 현상을 발견하였다. 이를 해결해보고자 한다.
MVVM 적용 시
@HiltViewModel
class ExampleViewModel
@Inject
constructor(
// inject value
) : ViewModel() {
private val _state = MutableStateFlow<ExampleState>(ExampleState.Init)
val state = _state.asStateFlow()
// ...
}
- Compose에서 해당 방식을 이용하면 nav가 이동하고 돌아왔을 때도 동일한 상태가 유지되는 현상 발생
- 해당 현상의 경우 hiltViewModel()이 navBackStackEntry의 생명주기를 따르기 때문에 StateFlow의 마지막 값이 유지되는 것으로 확인
MVI 적용
- User는 Action을 통해 Intent(event)를 발생시킴
- Intent는 Model(UIState)를 업데이트
- Model에 대해 View가 Recomposition될 때 SideEffect 발생
구현
interface UIState
interface UISideEffect
interface UIIntent
abstract class BaseViewModel<S : UIState, SE : UISideEffect, I : UIIntent>(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val initialState: S by lazy { createInitialState(savedStateHandle) }
protected abstract fun createInitialState(savedStateHandle: SavedStateHandle): S
protected abstract suspend fun handleIntent(intent: I)
private val _state = MutableStateFlow<S>(initialState)
val state = _state.asStateFlow()
private val _sideEffect = MutableSharedFlow<SE>()
val sideEffect = _sideEffect.asSharedFlow()
protected val currentState: S
get() = _state.value
protected val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
handleClientException(throwable)
}
fun intent(intent: I) {
launch {
handleIntent(intent)
}
}
protected fun reduce(reduce: S.() -> S) {
val state = currentState.reduce()
_state.value = state
}
protected fun postSideEffect(sideEffect: SE) {
viewModelScope.launch { _sideEffect.emit(sideEffect) }
}
protected inline fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
crossinline action: suspend CoroutineScope.() -> Unit,
): Job = viewModelScope.launch(
context = context + coroutineExceptionHandler,
start = start
) {
action()
}
abstract fun handleClientException(throwable: Throwable)
}
- initialState
- initialized된 uistate model
- createInitialState(savedStateHandle: SavedStateHandle)
- savedStateHandle.toRoute<Navigator>() 를 이용해서 초기 State를 init
- _state
- MutableStateFlow로 단일 상태 보장
- _sideEffect
- MutableSharedFlow로 단발성 Event 발행
- Channel이 아닌 SharedFlow로 작성한 이유
- 구독자가 없어도 event를 발행할 수 있는 hot stream을 선택
- intent
- UIIntent의 impl를 통해 User Event에 대한 값 획득
- handleClientException
- 발생한 오류를 handle하는 위치
반응형
'Android > Compose' 카테고리의 다른 글
Compose Multiplatform - shadow open source contribute 도전기 (5) | 2024.11.17 |
---|---|
아이나비 맵 SDK Compose로 사용해보기 (1) | 2024.09.02 |
[안드로이드] Inner Shadow, Drop Shadow 처리하는 방법 (1) | 2024.06.10 |
[안드로이드] Jetpack Compose UI Test 맛보기 (0) | 2024.05.12 |
[안드로이드] Compose 밑줄 텍스트와 클릭 가능하게 만들기 (0) | 2024.05.04 |