https://velog.io/@evergreen_tree/Android-MVI-%ED%8C%A8%ED%84%B4
이전에 MVI에 대해 작성했던 글입니다. 연관되는 내용이 많으니, 보고 오시면 이해가 더 쉽습니다.
필자의 개인의 주관적인 생각이 섞여있습니다. 오타, 반박 언제나 환영합니다!
UniDirectional Data Flow의 약자로, 단방향 데이터 흐름을 의미합니다.
Android의 경우 Activity에 View, Data, Logic이 겹치는 경우 유지보수가 힘들고 테스트가 어렵기 때문에 이전부터 View와 Data를 분리시키려는 노력이 있었습니다. 현재는 AAC Viewmodel의 활약으로 MVVM 등의 아키텍처가 성행하고 있고, 그만큼의 장점을 가져오고 있습니다.
UDF를 이러한 현재 View Data 분리 시스템에 녹일 수 있는데요,
UDF의 규약을 가지고 태어난 3개의 아키텍처로 Flux, Redux, MVI가 있습니다.
YAPP 21기로 활동하면서 참여한 프로젝트 티미티미에서는 Redux 아키텍처를 채택하여 앱을 개발하였는데요, 왜 선택하게 되었고 어떻게 적용했는지 공유하려고 합니다.
UDF를 적용하면 이러한 장점을 얻을 수 있습니다.
물론, 단점도 있습니다.
저도 해당 아키텍처를 적용하면서, 1번의 문턱을 넘기 위해 꽤나 많은 노력을 하였고, 프로젝트를 진행하면서 2번에도 많은 고통을 받았습니다.
위에서 말했듯, UDF의 규약을 가지고 태어난 3개의 아키텍처로 Flux, Redux, MVI가 있다고 하였는데요, 각각의 아키텍처 또한 서로 다른 차이점을 가지고 있습니다.
ViewModel이 가지고 있는 비즈니스 로직을 Reducer에 위임하면서, ViewModel이 책임지는 행위를 줄이고, 발생할 수 있는 문제를 디버깅할때 더 쉽게 찾을 수 있도록 Redux를 채택하게 되었습니다.
ViewModel에서 Intent를 받아, IntentFlow에 데이터를 발행 (View에서 ViewModel)
fun dispatch(intent: INTENT) {
viewModelScope.launch {
_mutableIntentFlow.emit(intent)
}
}
open fun start() {
val initialViewState = getInitialState()
viewState = _mutableIntentFlow
.mutate()
.filter { registerReducer() != null }
.scan(initialViewState) { prevState, mutate ->
registerReducer()!!.invoke(mutate, prevState)
}
.catch { processError(it) }
.stateIn(
viewModelScope,
// 바로 생산 시작.
SharingStarted.Eagerly,
initialViewState
)
}
mutate()
middleware에서 mutate 작업을 수행한 후 다시 intentflow에 병합됩니다. 이는 스트림의 가장 마지막 데이터를 반환하게 됩니다.
private fun SharedFlow<INTENT>.mutate(): Flow<INTENT> {
val middlewareList = registerMiddleware()
return if (middlewareList.isEmpty()) {
this
} else {
middlewareList
.scan(this.filterNotNull()) { prevIntentFlow, nextMiddleware ->
merge(
nextMiddleware.mutate(viewModelScope, prevIntentFlow, _singleEventFlow),
prevIntentFlow
)
}.last()
}
}
MiddleWare은 말그대로 중개자의 역할을 하는데, Intent와 Event를 받아서 상태(State)를 업데이트 할 것인지, SideEffect를 호출할지를 결정하게 됩니다.
interface BaseMiddleware<INTENT : BaseIntent, EVENT: BaseSingleEvent> {
fun mutate(scope: CoroutineScope, intentFlow: Flow<INTENT>, eventFlow: MutableSharedFlow<EVENT>): Flow<INTENT>
}
피쳐에서 정의한 Intent에 대해 onEach로 SideEffect를 발생시킵니다.(여기서 event를 방출) 그리고 HotFlow로 변환하여 다시 intenFlow에 병합하게 됩니다.
override fun mutate(
scope: CoroutineScope,
intentFlow: Flow<CreateProjectIntent>,
eventFlow: MutableSharedFlow<CreateProjectSingleEvent>
): Flow<CreateProjectIntent> {
return intentFlow.run {
merge(
filterIsInstance<CreateProjectIntent.ChangeProjectName>()
.onEach {
Timber.e(it.toString())
}
.shareIn(scope, SharingStarted.WhileSubscribed()),
...
reducer에서는 이전 상태를 가지고 비즈니스 로직을 수행하게 되는데, 이 과정에서 상태 변경이 일어날 수 있게 됩니다.
.scan(initialViewState) { prevState, mutate ->
registerReducer()!!.invoke(mutate, prevState)
}
reducer를 통해 새로운 상태를 정의하는 과정입니다.
override fun invoke(action: BaseIntent, state: CreateProjectState): CreateProjectState {
var newState = state
when (action) {
is CreateProjectIntent.ChangeProjectName -> {
newState = newState.copy(
projectName = action.name
)
}
.catch { processError(it) }
과정에서 발생하는 에러를 캐치하고, 처리할 수 있습니다.
또한 StateFlow로 변환하여 View에서 ViewState를 관찰하고, update 할 수 있는 환경을 제공합니다.
참고 : https://medium.com/@maryangmin/data-uni-directional-architecture-in-android-데이터-단방향-구조-fd7ef26e80fc
내공냠냠