์บก์คํค 1 ๋ฆฌํฉํ ๋ง - ViewModel๊ณผ StateFlow๋ก ๋ค์ ์ ๋ฆฌํ๊ธฐ
์ง๋ ์ธ ๊ธ(์ํ ํธ์ด์คํ โ ViewModel โ StateFlow/UiState)์ ๊ฑฐ์น๋ฉด์ to-do ์ฑ์ด ์กฐ๊ธ์ฉ ๋ฐ๋์๋ค. ์ด๋ฒ ๊ธ์ ์ ๊ฐ๋ ์ ์๋ค. ์ง๊ธ๊น์ง ๋ฐ๋ ๊ฑธ ํ๊ณณ์ ๋ชจ์์, ์ฒ์(7๋ฒ ๊ธ)๊ณผ ์ง๊ธ์ ๋๋ํ ๋๊ณ ๋ณด์.
๋ญ๊ฐ ๋ฌ๋ผ์ก๋
- 7๋ฒ:
todoList๊ฐTodoListScreen์์remember๋ก ๊ฐํ ์์๋ค. ์ฒดํฌ ์ํ๋LazyColumn๊ฐ ํญ๋ชฉ ์์ ๋ ๋ฐ๋กremember๋ก ์์๋ค. ์์ธ ํ๋ฉด์id์ซ์๋ง ๋ฐ๊ณ ์๋ฌด๊ฒ๋ ๋ชป ๋ณด์ฌ์คฌ๋ค. - ์ง๊ธ:
todoList๋TodoViewModel์StateFlow<TodoUiState>์์ ์๋ค. ๋ชฉ๋ก ํ๋ฉด, ์์ธ ํ๋ฉด ๋ ๋ค ๊ฐ์uiState๋ฅผ ๊ตฌ๋ ํด์ ๋ณธ๋ค. ํ๋ฉด์ ํ์ ํด๋ ๋ฐ์ดํฐ๊ฐ ์ ๋ ์๊ฐ๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พธ๋ ๊ธธ์addTodo/toggleDone/deleteTodo์ธ ํจ์๋ก๋ง ์ขํ์ ธ ์๋ค.
์ปดํฌ์ ๋ธ ์ชฝ(TodoListScreen, TodoDetailScreen)์ 8๋ฒ ๊ธ์์ ์ด๋ฏธ "๊ฐ + ์ฝ๋ฐฑ๋ง ๋ฐ๋ ๊ป๋ฐ๊ธฐ"๋ก ๋ง๋ค์ด๋๊ธฐ ๋๋ฌธ์, ๊ทธ ๋ค๋ก๋ ๊ฑฐ์ ์ ๋ฐ๋์๋ค. ์ํ๋ฅผ ์ด๋์ ๋๋๋๋ง remember โ TodoApp์ remember โ ViewModel์ StateFlow๋ก ๊ณ์ ์ฎ๊ฒจ ๋ค๋ ๊ฑฐ๋ค.
์ ์ฒด์ฝ๋
package com.buyoungsil.todo_project
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.buyoungsil.todo_project.ui.theme.Todo_projectTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class Todo(
val id: Int,
val title: String,
val isDone: Boolean = false
)
data class TodoUiState(
val todoList: List<Todo> = emptyList(),
val isLoading: Boolean = false
)
class TodoViewModel : ViewModel() {
private val _uiState = MutableStateFlow(
TodoUiState(
todoList = listOf(
Todo(id = 1, title = "์ฅ๋ณด๊ธฐ"),
Todo(id = 2, title = "์ด๋ํ๊ธฐ"),
Todo(id = 3, title = "์ฑ
์ฝ๊ธฐ")
)
)
)
val uiState: StateFlow<TodoUiState> = _uiState.asStateFlow()
fun addTodo(title: String) {
if (title.isBlank()) return
val newTodo = Todo(id = (_uiState.value.todoList.maxOfOrNull { it.id } ?: 0) + 1, title = title)
_uiState.value = _uiState.value.copy(todoList = _uiState.value.todoList + newTodo)
}
fun toggleDone(id: Int) {
val updated = _uiState.value.todoList.map {
if (it.id == id) it.copy(isDone = !it.isDone) else it
}
_uiState.value = _uiState.value.copy(todoList = updated)
}
fun deleteTodo(id: Int) {
_uiState.value = _uiState.value.copy(
todoList = _uiState.value.todoList.filter { it.id != id }
)
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Todo_projectTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
TodoApp()
}
}
}
}
}
}
@Composable
fun TodoApp(viewModel: TodoViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "list") {
composable("list") {
TodoListScreen(
todoList = uiState.todoList,
onAdd = viewModel::addTodo,
onToggleDone = viewModel::toggleDone,
onDelete = viewModel::deleteTodo,
onTodoClick = { todo -> navController.navigate("detail/${todo.id}") }
)
}
composable("detail/{todoId}") { backStackEntry ->
val todoId = backStackEntry.arguments?.getString("todoId")?.toIntOrNull()
val todo = uiState.todoList.find { it.id == todoId }
TodoDetailScreen(
todo = todo,
onToggleDone = viewModel::toggleDone,
onBack = { navController.popBackStack() }
)
}
}
}
@Composable
fun TodoListScreen(
todoList: List<Todo>,
onAdd: (String) -> Unit,
onToggleDone: (Int) -> Unit,
onDelete: (Int) -> Unit,
onTodoClick: (Todo) -> Unit
) {
var inputText by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
TextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("ํ ์ผ ์
๋ ฅ") }
)
Button(onClick = {
onAdd(inputText)
inputText = ""
}) {
Text("์ถ๊ฐ")
}
}
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(todoList, key = { it.id }) { todo ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.clickable { onTodoClick(todo) },
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = todo.isDone, onCheckedChange = { onToggleDone(todo.id) })
Text(text = todo.title, modifier = Modifier.weight(1f))
IconButton(onClick = { onDelete(todo.id) }) {
Icon(imageVector = Icons.Default.Delete, contentDescription = "์ญ์ ")
}
}
}
}
}
}
@Composable
fun TodoDetailScreen(
todo: Todo?,
onToggleDone: (Int) -> Unit,
onBack: () -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
if (todo == null) {
Text("ํด๋น ํ ์ผ์ ์ฐพ์ ์ ์๋ค")
} else {
Text(text = todo.title, style = MaterialTheme.typography.titleLarge)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = todo.isDone, onCheckedChange = { onToggleDone(todo.id) })
Text(if (todo.isDone) "์๋ฃ" else "๋ฏธ์๋ฃ")
}
}
Button(onClick = onBack) {
Text("๋ค๋ก ๊ฐ๊ธฐ")
}
}
}[์ด๋ฏธ์ง: ์์ฑ๋ to-do ์ฑ โ ๋ชฉ๋ก ํ๋ฉด์์ ์ถ๊ฐ, ์ฒดํฌ, ์์ธ ์ง์ ๊น์ง ํ ํ๋ฉด์ ๋ณด์ด๋ ๋ชจ์ต]
์ ๋ฆฌ
- ์ํ๋ฅผ ์ด๋์ ๋๋๋(
rememberโ ์์ ์ปดํฌ์ ๋ธ โViewModel)๋ ๋ฐ๋์์ง๋ง, ํ๋ฉด์ ๊ทธ๋ฆฌ๋TodoListScreen/TodoDetailScreen์ฝ๋๋ ์ฒ์ ์ํ ํธ์ด์คํ ์ ํด๋ ๋ค๋ก ๊ฑฐ์ ๊ทธ๋๋ก์๋ค - ์ง๊ธ ๋ฒ์ ์ ํ๋ฉด ํ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์ ๋ ์๊ฐ๊ณ , ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พธ๋ ๊ธธ์ด ์ธ ํจ์๋ก ์ขํ์ ธ ์๊ณ , ๋ก๋ฉ ์ํ(
isLoading)๋ฅผ ์ถ๊ฐํ ์๋ฆฌ๋UiState์ ์ด๋ฏธ ๋ง๋ จ๋ผ ์๋ค - ์ฌ๊ธฐ๊น์ง๊ฐ "์ปดํฌ์ฆ ํ๋ฉด ๊ตฌ์กฐ"์ ๊ธฐ๋ณธ ํ์ด๋ค. ๋ค์๋ถํฐ๋ ์ด ํ ์์ ๋ค์ด๊ฐ๋ ๋ฐ์ดํฐ๋ฅผ ์ด๋์ ๊ฐ์ ธ์ค๋์ง๋ฅผ ๋ค๋ฃฌ๋ค
์ง๊ธ๊น์ง๋ todoList๊ฐ ์ฑ์ ์คํํ ๋๋ง๋ค ์ฝ๋ ์์ ๊ณ ์ ๊ฐ(์ฅ๋ณด๊ธฐ, ์ด๋ํ๊ธฐ, ์ฑ
์ฝ๊ธฐ)์ผ๋ก ๋ค์ ์ฑ์์ก๋ค. ์ฑ์ ๊ป๋ค ์ผ๋ ํ ์ผ์ด ๋จ์์๊ฒ ํ๋ ค๋ฉด ๋์คํฌ์ ์ ์ฅํด์ผ ํ๋ค. ๋ค์ ๊ธ๋ถํฐ๋ Room์ผ๋ก ์ด ๋ฐ์ดํฐ๋ฅผ ์ค์ ๋ก์ปฌ DB์ ์ ์ฅํด๋ณด๊ฒ ๋ค.
