상태 관리
안드로이드는 어떠한 관점에선 다양한 상태의 집합인데, 화면에 나타나는 정보, 버튼 활성화 등 다양한 상태들로 구성되어 있다.
이러한 상태들이 많아질수록 관리하기 힘들어지고, 의도하지 않는 방향으로 제어가 된다면 네트워크 통신이 실패했음에도 프로그레스바가 지속적으로 보여진다든지와 같은 문제가 발생할 수 있다.
Compose
를 사용하게 된다면, Compose
의 업데이트는 상태의 변경(Recomposition
)에 의해서 이루어지기 때문에 상태 관리의 중요성은 더욱 더 커지게 된다.
MVI
MVI
디자인 패턴은 Model
, View
, Intent
의 줄임말이며, 여기서 Intent
는 안드로이드의 Intent
를 말하는 것이 아니다.
MVI
는 단방향 구조를 가지고 있는데, 예를 들어, 사용자가 새롭게 View
를 갱신하고자 한다면, View
를 갱신하고자 하는 의도(Intent
)가 새로운 Model
을 생성하게 되고, 이것이 상태를 업데이트 하게 되고 뷰에 반영되는 흐름을 가진다.
MVI
의 Model
은 변경 불가한 데이터로, UI
에 상태를 표현하는 반영될 상태를 의미하며, 앱의 상태는 단방향 흐름에서 Intent
로부터 Model
을 생성할 때만 새로운 Model
을 생성한다.
이러한 구조를 가졌기 때문에 예측 가능한 상태를 설정할 수 있고, 디버깅이 쉬워진다.
Intent
는 앱의 상태를 바꾸고자 하는 의도를 말하는데, Model
은 이 Intent
를 통해서 새로운 상태로 변화할 수 있다.
Pure Cycle
MVI
는 순수 함수로 이루어진 Pure
Cycle
로 표현할 수 있는데, 순수 함수는 어떤 함수에 동일한 인자를 주었을 때 항상 같은 값을 리턴하는 함수를 말한다.
Pure
Cycle
은 intent(event) == state
을 만족하는 사이클로, 어떠한 의도로 인해 발생하는 결과는 항상 상태이다.
// 1. 뷰에 렌더링 할 값인 total 을 가지는 Model(State)을 생성
data class NumberState (
val num: Int = 0
)
// 2. ViewModel 생성 및 Intent 정의(Used Orbit)
class NumberViewModel : ViewModel(), ContainerHost<NumberState, Unit> {
// ViewModel 에서 State 와, SideEffect 를 가지고 container 를 재정의
// container 선언에는 State 와 SideEffect 를 명시해야 함
override val container =
container<NumberState, Unit>(NumberState())
// add 함수를 intent 에 담고, 연산된 결과를 reduce 를 통해 새로운 state 로 만듬
// reduce 는 순수 함수로, 새로운 state 를 만듬
fun add(num : Int) = intent {
reduce {
state.copy(num = state.num + num)
}
}
}
// 3. Rendering
@Composable
fun NumberScreen(
viewModel: NumberViewModel
) {
// 상태 관찰
val state by viewModel.container.stateFlow.collectAsState()
Column {
Button(
onClick ={
// 버튼의 이벤트로 viewModel.add() 를 전달하게 되면
// ViewModel 에서 Intent 로 감싸져 새로운 상태를 생성
viewModel.add(1)
}
)
Text("숫자: ${state.num}")
}
}
Side Effect Cycle
MVI
는 Pure
Cycle
에 부수 효과가 포함되어 있는 Side
Effect
Cycle
로도 표현할 수 있는데, 부수 효과는 외부의 상태를 변경하는 것으로, 함수로 들어온 인자의 상태를 직접 변경하는 것을 말한다.
Intent
를 통해 새로운 상태를 생성함과 동시에 부수 효과를 실행하면, 인텐트로 결과를 내보내거나 아무 일도 일어나지 않는다.
// 1. Side Effect 정의
sealed class NumberSideEffect {
data class Toast(val text: String): NumberSideEffect()
}
data class NumberState (
val num: Int = 0
)
// 2. Post SideEffect
class NumberViewModel : ViewModel(), ContainerHost<NumberState, NumberSideEffect> {
// ViewModel 에서 State 와 SideEffect 를 가지고 container 를 재정의
override val container =
container<NumberState, NumberSideEffect>(NumberState())
// add 함수를 intent 에 담고, Toast 라는 SideEffect 에
// Toast 에 들어갈 메시지를 담아 SideEffect 를 보냄
fun add(num : Int) = intent {
postSideEffect(
NumberSideEffect.Toast("$number + ${state.num}")
)
// 연산된 결과를 reduce 를 통해 새로운 state 를 만들어 내는 과정은 Pure Cycle 과 동일
reduce {
state.copy(num = state.num + num)
}
}
}
// 3. Launch SideEffect
@Composable
fun NumberScreen(
viewModel: NumberViewModel
) {
/* ... */
// Composable 함수에서 LaunchedEffect 블록 안에서, SideEffect를 수집하고, Toast를 실행
LaunchedEffect(viewModel) {
viewModel.container.sideEffectFlow.collect {
when(it) {
is CalculatorSideEffect.Toast -> {
Toast.makeText(context, it.text, Toast.LENGTH_SHORT).show()
}
}
}
}
}
MVI 장점 및 단점
장점
- 상태 관리가 쉽다.
- 데이터의 흐름이 단방향이기 때문에 흐름을 이해하고 관리하기 쉽다.
- 각각 값이 불변하기 때문에, 스레드 안정성을 보장한다.
- 디버깅 및 테스트가 쉽다.
단점
- 러닝 커브가 높다.
- 작은 변경도
Intent
로 처리해야 하기 때문에, 이로 인한 보일러 플레이트 코드가 양산된다. Intent
,State
,Side
Effect
등 모든 상태에 대한 객체를 생성해야 하므로GC
가 빈번히 일어날 수 있어서 메모리 관리에 유의해야 한다.