[Kotlin] 12. 상태관리 - ViewModel, State
Android 앱에서 데이터를 효율적으로 관리하는 ViewModel과 상태 관리를 배웁니다.
ViewModel이란?
화면 회전 등 구성 변경에도 데이터를 유지하는 컴포넌트입니다.
| 항목 | remember | ViewModel |
|---|---|---|
| 생존 범위 | Composable | Activity/Fragment |
| 화면 회전 | 데이터 소실 | 데이터 유지 |
| 용도 | UI 상태 | 비즈니스 로직 + 데이터 |
의존성
// build.gradle.kts
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0")
}
ViewModel 기본
카운터 예제
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value++
}
fun decrement() {
_count.value--
}
fun reset() {
_count.value = 0
}
}
Composable에서 사용
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.collectAsState
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("$count", fontSize = 48.sp)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { viewModel.decrement() }) { Text("-") }
Button(onClick = { viewModel.reset() }) { Text("초기화") }
Button(onClick = { viewModel.increment() }) { Text("+") }
}
}
}
Todo 앱 ViewModel
모델
data class Todo(
val id: Int,
val title: String,
val isDone: Boolean = false
)
ViewModel
class TodoViewModel : ViewModel() {
private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos: StateFlow<List<Todo>> = _todos.asStateFlow()
private var nextId = 1
val doneCount: Int get() = _todos.value.count { it.isDone }
val totalCount: Int get() = _todos.value.size
fun addTodo(title: String) {
if (title.isBlank()) return
val newTodo = Todo(id = nextId++, title = title.trim())
_todos.value = _todos.value + newTodo
}
fun toggleTodo(id: Int) {
_todos.value = _todos.value.map { todo ->
if (todo.id == id) todo.copy(isDone = !todo.isDone) else todo
}
}
fun deleteTodo(id: Int) {
_todos.value = _todos.value.filter { it.id != id }
}
}
UI
@Composable
fun TodoScreen(viewModel: TodoViewModel = viewModel()) {
val todos by viewModel.todos.collectAsState()
var inputText by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(
title = { Text("할일 (${viewModel.doneCount}/${viewModel.totalCount})") }
)
}
) { padding ->
Column(modifier = Modifier.padding(padding)) {
// 입력 영역
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("할일 입력") },
singleLine = true
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
viewModel.addTodo(inputText)
inputText = ""
}) {
Text("추가")
}
}
// 목록
LazyColumn {
items(todos, key = { it.id }) { todo ->
TodoItem(
todo = todo,
onToggle = { viewModel.toggleTodo(todo.id) },
onDelete = { viewModel.deleteTodo(todo.id) }
)
}
}
}
}
}
@Composable
fun TodoItem(todo: Todo, onToggle: () -> Unit, onDelete: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = todo.isDone, onCheckedChange = { onToggle() })
Text(
text = todo.title,
modifier = Modifier.weight(1f),
style = if (todo.isDone) {
TextStyle(textDecoration = TextDecoration.LineThrough, color = Color.Gray)
} else TextStyle()
)
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, "삭제", tint = Color.Red)
}
}
}
}
UI State 패턴
sealed class로 상태 표현
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()
init {
loadUsers()
}
private fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val users = repository.getUsers()
_uiState.value = UiState.Success(users)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "알 수 없는 오류")
}
}
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (val state = uiState) {
is UiState.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is UiState.Success -> {
LazyColumn {
items(state.data) { user ->
Text(user.name)
}
}
}
is UiState.Error -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("에러: ${state.message}", color = Color.Red)
}
}
}
}
- [Kotlin] 18. 빌드와 배포 - Gradle, APK, JAR
- [Kotlin] 17. 실전 팁 - 자주 쓰는 패턴과 관용구
- [Kotlin] 16. 테스트 - JUnit, 단위 테스트
- [Kotlin] 15. 서버 개발 - Spring Boot with Kotlin
- [Kotlin] 14. 로컬 저장소 - Room, DataStore
- [Kotlin] 13. 네트워크 통신 - Retrofit
- [Kotlin] 12. 상태관리 - ViewModel, State
- [Kotlin] 11. 화면 이동 - Navigation
- [Kotlin] 10. Compose 레이아웃과 리스트
- [Kotlin] 09. Android 개발 기초 - 프로젝트 생성
- [Kotlin] 08. 코루틴 - 비동기 프로그래밍
- [Kotlin] 07. Null 안전성과 예외 처리
- [Kotlin] 06. 컬렉션 - List, Map, Set
- [Kotlin] 05. 클래스와 객체지향 프로그래밍
- [Kotlin] 04. 함수 - 선언, 매개변수, 람다
- [Kotlin] 03. 제어문 - 조건문, 반복문
- [Kotlin] 02. 변수와 데이터 타입
- [Kotlin] 01. Kotlin 소개 및 개발환경 설치