개발관련일지

android mvi학습 및 샘플코드 작성 본문

카테고리 없음

android mvi학습 및 샘플코드 작성

BEECHANGBOT 2023. 8. 2. 23:14

mvi는 먼저 사용한 곳은 Cycle.js 프레임워크이며 함수형 및 리엑티브 형식이다. 해당 프레임워크 창작자는 사용자 상호작용을 리엑티브 함수형으로 처리하기 위해서 MVI 패턴 제안하는거고 안드로이드로 넘어온걸로 보인다.
view model intnt를 함수로 나누고 리엑티브로 구성해 서로를 관찰하고 응답하도록 하면서 데이터 흐름이 단반향으로 순환되도록하는게 특징이다.
 
안드로이드 compose에서 ui갱신이 안된다던지 recomposition이 빈번하게 일어나고 결국 상태를 관리하는게 중요해졌고 상태를 관리하기위해 채택되었다. 안드로이드에서는 view (activity,fragment) <-> ViewModel를 이용해서 상태를 관리하고 있다.
MVI는 모델, 인텐트, 뷰 로 되어있는데 유저와의 상호작용하는 과정을 생각해보면 이러한 컨셉을 이해할 수 있다.
유저가 무언가를 행동(이벤트) -> 유저 이벤트 의도파악 및 요청 -> 요청 결과가 모델로 입력 -> 모델이 새로운 state를 생성 후 view로 입력 user -> intent -> model -> view -> user -> intent -> model 로 반복된다.
 
Intent : 앱의 상태를 바꾸려는 의도이며 유저의 이벤트 호출이 결과로 발생한다.
Model : UI에 반영될 상태State)를 의미한다. State는 불변적인 데이터구조이고 이미 존재하는 모델을 업데이트 하는게 아닌 새로운 모델이 된다. State를 바꿀수 있는 시작점은 Intent가 유일해야한다.
View : 엑티비티 , 프래그먼트 이며 Model의 상태에 따라서 랜더링한다.
 
샘플코드들을 많이 참고했고 내가 생각하는 mvi에 맞도록 샘플들 짜깁기해서 간단하게 만들어 봤다.
 

class MainViewModel(
    private val repository: GetFruitRepository = GetFruitRepository()
) : ViewModel() {

    private val mainEvent = MutableSharedFlow<MainEvent>()

    val mainState: StateFlow<MainState> = mainEvent
        .transform { event -> handleEvent(event, this) } //사이드이팩트 확인
        .map { event -> processBusinessLogic(event) }  // 비즈니스 로직 처리
        .scan(MainState(), ::reduce) // 상태생성
        .stateIn(viewModelScope, SharingStarted.Eagerly, MainState())

    private val _sideEffects = Channel<MainEffect>()
    val sideEffects = _sideEffects.receiveAsFlow()

    suspend fun intent(event: MainEvent) = mainEvent.emit(event) //1. 유저이벤트 트리거

    private suspend fun handleEvent(event: MainEvent, collector: FlowCollector<MainEvent>) {
        when (event) { 
            is MainEvent.Toast -> { //사이드 이팩트인경우 사이드이팩트 스트림으로 처리
                _sideEffects.send(MainEffect.Toast("toast start"))
            }

            is MainEvent.Count, is MainEvent.GetFruit -> {
                collector.emit(event)
            }
        }
    }

    private suspend fun processBusinessLogic(event: MainEvent): MainEvent {
        return when (event) {
            is MainEvent.Count -> event.copy(count = event.count + 1)
            is MainEvent.GetFruit -> {
                if (Random.nextFloat() <= 0.7) {
                    event.copy(fruit = repository.getData())
                } else {
                    _sideEffects.send(MainEffect.Fail)
                    event
                }
            }
            else -> event
        }
    }

    private fun reduce(current: MainState, event: MainEvent): MainState {
        return when (event) {  
            is MainEvent.Count -> current.copy(count = event.count)
            is MainEvent.GetFruit -> current.copy(fruit = event.fruit)
            else -> {
                current
            }
        }
    }
}

mainEvent라는 데이터 스트림을 생성해놓고 단방향으로 데이터가 흐를수있도록 사용했다. 
크게보면 유저이벤트중에 사이드이팩트가 있는지 확인하고 처리 -> 비즈니스로직처리 -> 상태업데이트 이고 
Reducer를 어떻게 만들어야 하나 감이 좀 안잡혀서 찾아보았을때 웹동네의 리듀서를 강조하는 원칙은 순수함수(이전상태 + 동작 = 새상태) -> 불변 , 새로운 State생성 이 필요하다고 했다.
처음엔 reduce함수에 비즈니스 로직까지 포함 했었는데 비즈니스처리를 추가해서 순수하게 상태와 이벤트만으로 상태관리를 하도록 작성했다.
참고한 코드들을 뜯어보면서 만들고 틀린부분없나 불안한 부분들을 리덕스쪽가서 개념적으로 다시 학습했다. 
 
학습용으로 만들긴 했지만 사용되는 이벤트, 상태 데이터 흐름들은 추상화해서 사용은 필수라 생각한다. 테스트도 신경안쓰고 실적용 코드가 아닌데도 이정도면 작성해야되는 코드가 많이 늘었다고 느꼈다. 추후엔 제공되는 MVI 라이브러리들 Orbit, Maverick등을 사용해봐야겠다. 
 
완성코드 
 
 
참고 
https://youtu.be/_ePUpzECd8c
https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow
https://github.com/charlezz/mviexample
https://github.com/kimdohun0104/kinda
https://github.com/KrishanMadushanka/MVI-Demo