UI를 중심의 관심사 분리를 목표로 한다. 세 가지 구성 요소로 이루어진다.
MVI의 가장 큰 특징은 순수 함수 기반의 사이클을 따른다는 것이다.
Intent()의 호출 결과가 Model()의 인자로 전달된다. Model()의 호출 결과가 View()의 인자로 전달된다.
순수 함수
다음 두 가지 조건을 만족하는 함수이다.
1. 함수의 입력이 같다면 항상 같은 출력을 반환한다.
2. Side Effect가 없다. (외부 상태를 변경하거나 의존하지 않는다.)
MVI는 데이터가 한 방향으로만 흐르는 단방향 데이터 흐름을 갖는다.
예를 들어,
1. 사용자가 View에서 목록을 클릭하면, 데이터를 가져오려는 Intent가 생성된다.
2. 해당 Intent가 처리되어 Model에서 새로운 데이터를 생성한다.
3. 생성된 데이터는 View에 반영된다.
이러한 단방향 데이터 흐름은, Compose의 철학과도 부합한다.

MVI는 MVVM과 완전히 별개의 패턴이 아닌, MVVM에서 발전된 형태로 볼 수 있다.
즉, MVVM 내에서 구현될 수 있는 하위 패턴으로 볼 수 있다.

MVI에서 Intent 호출 결과는 항상 새로운 상태(Model)를 생성한다.
MVI는 순수함수 사이클을 지향하기에 상태를 불변하게 관리하며, 상태를 변경하기 위해 항상 새로운 상태를 생성한다.
State Reducer는 주어진 현재 상태와 이벤트를 기반으로 새로운 상태를 생성하는 역할을 한다.
이 방식은 모든 상태를 한 곳에서 관리하기 때문에 디버깅이 용이해진다.
// Intent에 해당
sealed class Event {
object Increment: Evnet()
object Decrement: Event()
}
// Model에 해당
data class State(val counter: Int = 0)
class ViewModel {
// Event 처리 순서를 보장한다.
private val events = Channel<Event>()
// 새로운 State를 생성한다.
val state = events.receiveAsFlow()
.runningFold(State(), ::reduceState) // 이벤트와 현재 상태를 통해 새로운 상태를 생성하는 State Reducer
.staeln(viewModelScope, Eagerly, State())
fun handleEvent(event: Event) {
when(event) {
is Increment -> state.update { it.copy(counter = it.counter + 1) }
is Decrement -> state.update { it.copy(counter = it.counter + 1) }
}
}
}
순수함수로만 앱을 구성하기는 어렵다. 그렇기에 Side Effect를 통해 보완할 수 있다.
Side Effect는 Intent가 반드시 UI 상태에 반영되지 않아도 될 때 사용할 수 있다.
예를 들어,
버튼 클릭 시 상태를 변경하지 않고 사용자에게 간단한 알림(토스트)을 보여주거나, 특정 동작을 로깅할 때 Side Effect를 통해 처리할 수 있다.