일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 변수
- recyclerview item recycle
- 리사이클러뷰 아이템 재사용
- 객체지향 프로그래밍 5가지 원칙
- AAC
- java thread 예제
- apache란
- java
- 안드로이드 스튜디오 style
- 아파치란
- hilt error
- 이중for문 사용 안하기
- Kotlin
- savedinstancestate
- 안드로이드 스튜디오 커스텀 다이얼로그
- LifeCycle
- 안드로이드 스튜디오 반복되는 레이아웃 코드
- 자바 스레드 예제
- 안드로이드 스튜디오 인터넷 연결 확인
- 안드로이드 스튜디오 custom dialog
- 안드로이드 스튜디오 tts
- dagger error
- Thread
- 안드로이드 스튜디오 인터넷 연결 안되어 있을 때
- edittext 연결
- 디자인 패턴 예제
- 다른 객체 리스트의 비교
- 아파치 엔진엑스
- 안드로이드 디자인패턴
- apache nginx
- Today
- Total
Sam Story
안드로이드 MVI 패턴 본문
오늘은 지난번에 포스팅 했던 디자인 패턴들에 이어서
MVI 패턴에 대해서 포스팅 해보려 한다.
MVI 패턴이란 ?
MVI 패턴은 Model , View , Intent의 약자이다.
그럼 각 컴포넌트들이 어떤 역할을 하는지 알아보자.
Model
- 상태(State)를 관리
- 현재 UI의 상태를 나타내는 데이터이며, UI가 어떤 화면을 보여줄지를 결정
View
- 사용자에게 화면(UI)을 표시
- 사용자의 이벤트를 Intent로 전달
Intent
- 사용자의 이벤트나 의도를 전달
- View에서 발생한 이벤트를 Model에 전달
- Model이 새로운 상태를 생성하면 View에 다시 전달하여 화면을 업데이트
MVI 패턴의 특징
1. 단방향 데이터 흐름
데이터가 Intent → Model → View 순서로만 흐른다.
유저가 이벤트를 발생시키면 그 이벤트를 모델에 전달하고 모델이 새로운 상태를
생성하면 View에 다시 전달하여 화면을 업데이트 한다.
이 사이클이 계속 순환하게 되는 단방향 데이터 흐름을 갖는다.
아래 그림을 보면 좀 더 간단히 이해할 수 있다.

2. 상태(State) 중심 관리
UI 상태를 단일 객체로 관리한다.
모든 상태는 불변이며, 새로운 상태 객체를 생성하여 화면을 갱신하는 방식이다.
Compose 또한 상태(State)에 따라 UI를 자동으로 업데이트 하는데
Compose와 MVI 패턴의 State의 개념이 맞아떨어지고 코드 작성 자체가 단순화된다.
Compose와 MVI는 모두 상태를 중심으로 동작하며,
선언형 UI와 단방향 데이터 흐름을 기반으로 하기 때문에
자연스럽게 조화를 이루어 효율적이고 직관적인 앱 개발 환경을 제공한다.
3. 예제
오늘의 예제는 버튼을 누르면 Text의 카운트가 1씩 오르고 내리는 간단한 예제이다.
예제에 사용한 flow에 관해서는 추후에 포스팅 하도록 하겠다.
CounterIntent
package com.example.mvipatternexample
sealed class CounterIntent {
object Increment: CounterIntent()
object Decrement: CounterIntent()
}
사용자의 이벤트를 전달할 CounterIntent class를 생성하고
Increment , Decrement 인텐트 객체를 생성한다.
CounterState
package com.example.mvipatternexample
data class CounterState(val count: Int = 0)
화면에 표시할 데이터를 포함하는 State class를 생성해준다.
CounterViewModel
package com.example.mvipatternexample
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel: ViewModel() {
private val _state = MutableStateFlow(CounterState())
val state get() = _state.asStateFlow()
fun handleIntent(intent: CounterIntent) {
when(intent) {
is CounterIntent.Increment -> {
_state.value = _state.value.copy(count = _state.value.count + 1)
}
is CounterIntent.Decrement -> {
_state.value = _state.value.copy(count = _state.value.count - 1)
}
}
}
}
여기서 생성한 ViewModel이 MVI 패턴의 Model의 역할을 수행한다.
Screen (Composable)
package com.example.mvipatternexample
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
Log.d("스크린", "Screen Recomposition")
val state by viewModel.state.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CountText(count = state.count)
InfoText()
IncrementButton(viewModel = viewModel)
DecrementButton(viewModel = viewModel)
}
}
@Composable
fun CountText(count: Int) {
Log.d("스크린", "CountText Recomposition")
Text("Count: $count")
}
@Composable
fun InfoText() {
Log.d("스크린", "InfoText Recomposition")
Text("증가 = Increment / 감소 = Decrement",)
}
@Composable
fun IncrementButton(viewModel: CounterViewModel) {
Button(onClick = { viewModel.handleIntent(CounterIntent.Increment) }, modifier = Modifier.padding(15.dp)) {
Text("Increment")
}
}
@Composable
fun DecrementButton(viewModel: CounterViewModel) {
Button(onClick = { viewModel.handleIntent(CounterIntent.Decrement) }, modifier = Modifier.padding(15.dp)) {
Text("Decrement")
}
}
예제에 사용될 Composable 이다.
state 변수가 직접 사용되는 Text와 그렇지 않은 Text를 두고
버튼을 눌렀을 때 Recomposition이 어떻게 이루어지는지 확인하기 위해
로그를 작성했다.
MainActivity
package com.example.mvipatternexample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.example.mvipatternexample.ui.theme.MVIPatternExampleTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MVIPatternExampleTheme {
CounterScreen()
}
}
}
}
MainActivity에서 CounterScreen() 을 호출하게 되면 완성이다.
실행결과



버튼을 누를 때마다 Text가 변화하는걸 볼 수 있다.
그럼 찍은 로그를 확인해보자.

처음 화면이 구성될 때 Screen,CountText,InfoText 세가지 전부 로그가 찍히게 되고
버튼을 누를 때 Screen , CountText 두 로그만 찍히고
InfoText 로그가 찍히지 않는걸 확인할 수 있다.
이로그로 state가 변할때 관련있는 View만 Recomposition이 되고
그렇지 않은 View는 그대로 있는걸 확인할 수 있다.
이렇게 불필요한 Recomposition을 줄이고
필요한 View만 Recomposition 함으로써 굉장히 효율적인 코드를 작성할 수 있다는게
MVI의 장점인것 같다.
많은 기업들이 Compose로 마이그레이션 하거나 많이 사용하는 추세인만큼
MVI 패턴 적용에 대한 요구도 많아지고 있기에 공부해야 하는 내용이라 생각한다.
'Android' 카테고리의 다른 글
savedInstanceState (3) | 2024.10.30 |
---|---|
커스텀 다이얼로그 (custom Dialog) (0) | 2024.08.18 |
핸들러 (Handler) (0) | 2024.05.13 |
안드로이드 MVVM 패턴 (0) | 2024.04.30 |
안드로이드 MVP 패턴 (0) | 2024.04.29 |