Skip to content
Go back

Android MVI pattern

Android MVI pattern

1. MVI?

MVI는 Model-View-Intent의 약자, 단방향 데이터 흐름과 불변 상태를 기반으로 동작하는 아키텍처 패턴.

MVI의 주요 특징

  1. 단방향 데이터 흐름: 데이터가 항상 한 방향으로 흐름.
  2. 불변 상태 관리: 상태가 한번 생성되고난 후 수정되지 않는다. 업데이트시 새로운 상태 생성
  3. 높은 디버깅 가능성: 상태 변화가 명확하게 정의되어 있어 디버깅 및 테스트가 쉬움.

MVI vs. MVVM

특징MVVMMVI
데이터 흐름양방향 (데이터 바인딩 사용)단방향
상태 관리뷰모델에서 관찰 가능단일 상태 객체 관리
복잡도중간초기 설정 복잡
사용 사례간단한 앱, CRUD 애플리케이션복잡한 비즈니스 로직, 실시간 UI

2. MVI 사용시 이점

MVI의 장점

  1. 단일 진실 원칙: 상태가 하나의 소스에서 관리되어 UI와의 동기화 문제가 줄어듬.
  2. 예측 가능한 동작: 모든 상태 변화가 명시적으로 정의되므로 디버깅이 용이.
  3. 유지보수 용이성: 상태 관리와 UI 업데이트가 분리되어 유지보수가 쉽다.
  4. 리플레이 가능 이벤트: 상태와 이벤트를 저장해 재실행할 수 있다.

적용 사례

3. MVI 요소 분석

Model

Application의 상태를 정의. Data class로 설계되고 필요한 정보를 포함

data class ViewState(
    val isLoading: Boolean,
    val items: List<String>,
    val errorMessage: String? = null
)

View

View는 상태를 기반으로 UI를 그리고 사용자 이벤트를 인텐트로 변환하여 전달

Intent

사용자 액션이나 이벤트를 정의. 이를 통해 상태를 업데이트 하는 동작

sealed class UserIntent {
    object LoadItems : UserIntent()
    data class AddItem(val item: String) : UserIntent()
    data class RemoveItem(val index: Int) : UserIntent()
}

4. 예제 (TODO list 만들기)

1. view state 정의

data class TodoState(
    val todos: List<Todo> = emptyList(),
    val newTodoText: String = "",
    val isLoading: Boolean = false,
    val error: String? = null
)

data class Todo(
    val id: Int,
    val text: String,
    val isCompleted: Boolean = false
) 

2. Intent 정의

sealed class TodoIntent {
    data class AddTodo(val text: String) : TodoIntent()
    data class ToggleTodo(val id: Int) : TodoIntent()
    data class DeleteTodo(val id: Int) : TodoIntent()
    data class UpdateNewTodoText(val text: String) : TodoIntent()
    object ClearCompleted : TodoIntent()
} 

3. ViewModel 구현

class TodoViewModel : ViewModel() {
    private val _state = MutableStateFlow(TodoState())
    val state: StateFlow<TodoState> = _state.asStateFlow()
    
    private var nextId = 1

    fun processIntent(intent: TodoIntent) {
        when (intent) {
            is TodoIntent.AddTodo -> {
                if (intent.text.isNotBlank()) {
                    _state.update { currentState ->
                        val newTodo = Todo(id = nextId++, text = intent.text)
                        currentState.copy(
                            todos = currentState.todos + newTodo,
                            newTodoText = ""
                        )
                    }
                }
            }
            
            is TodoIntent.ToggleTodo -> {
                _state.update { currentState ->
                    val updatedTodos = currentState.todos.map { todo ->
                        if (todo.id == intent.id) {
                            todo.copy(isCompleted = !todo.isCompleted)
                        } else {
                            todo
                        }
                    }
                    currentState.copy(todos = updatedTodos)
                }
            }
            
            is TodoIntent.DeleteTodo -> {
                _state.update { currentState ->
                    currentState.copy(
                        todos = currentState.todos.filterNot { it.id == intent.id }
                    )
                }
            }
            
            is TodoIntent.UpdateNewTodoText -> {
                _state.update {
                    Log.e("hmjoo", "update text : ${intent.text}")
                    it.copy(newTodoText = intent.text) }
            }
            
            is TodoIntent.ClearCompleted -> {
                _state.update { currentState ->
                    currentState.copy(
                        todos = currentState.todos.filterNot { it.isCompleted }
                    )
                }
            }

            else -> {}
        }
    }
} 

4. View 연결

class ToDoActivity : AppCompatActivity() {
    private val viewModel: ToDoViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_todo)

        viewModel.state.observe(this) { state ->
            render(state)
        }

        // 사용자 동작 처리
        findViewById<Button>(R.id.addButton).setOnClickListener {
            val newItem = "New Task"
            viewModel.processIntent(ToDoIntent.AddItem(newItem))
        }
    }

    private fun render(state: ToDoViewState) {
        if (state.isLoading) {
            // 로딩 화면 표시
        } else {
            // UI 업데이트
        }
    }
}

Share this post on:

Previous Post
Compose Edge-to-Edge Dialog 구현
Next Post
Hugo에서 Jekyll로 이전하기