기초

실생활 앱 (할 일 관리)

로컬 목록 데이터를 추가, 조회, 수정, 완료, 삭제하는 Todo 앱을 만들며 CRUD와 저장 구조의 기초를 익힙니다.

동작

실생활 앱 (할 일 관리) 실행 화면

TextField에 할 일을 입력하고 추가합니다. 완료 버튼으로 상태를 바꾸고, 수정 버튼으로 제목을 고칩니다. 삭제 버튼으로 필요 없는 항목을 목록에서 제거합니다.

예제 코드

할 일 관리

할 일 관리 기초

예제 코드 기능별 설명

코드를 나누어 읽기

import

material.dart는 TextField, ElevatedButton, Checkbox, ListTile, IconButton처럼 Todo 앱을 구성하는 기본 위젯을 제공합니다. 이 예제는 별도 상태관리 패키지 없이 StatefulWidget과 setState만으로 CRUD 흐름을 먼저 익히는 구조입니다.

import 'package:flutter/material.dart';

Todo 모델

할 일은 제목만 있는 문자열이 아니라 완료 여부도 함께 가져야 합니다. 그래서 Todo 모델로 title과 done을 묶습니다. 모델을 만들면 UI, 저장소, 수정 로직이 모두 같은 데이터 구조를 기준으로 움직일 수 있습니다.

class Todo {
  const Todo({required this.title, this.done = false});
  final String title;
  final bool done;
}

copyWith

Todo는 불변 객체처럼 다루는 편이 안전합니다. 제목이나 완료 여부를 바꿀 때 기존 객체를 직접 수정하지 않고 copyWith로 새 객체를 만들어 목록에 다시 넣습니다. 이 방식은 나중에 Provider, Riverpod, Bloc 같은 상태관리로 옮길 때도 자연스럽습니다.

Todo copyWith({String? title, bool? done}) {
  return Todo(title: title ?? this.title, done: done ?? this.done);
}

입력 컨트롤러

controller는 TextField에 적힌 값을 읽고, 저장 후 입력창을 비우기 위해 사용합니다. 수정 모드에서는 선택한 Todo의 제목을 controller.text에 넣어 같은 입력창을 재사용합니다.

final controller = TextEditingController(text: '물 마시기');

목록 상태

todos는 현재 화면에 표시되는 로컬 데이터 목록입니다. 예제에서는 메모리 배열로 시작하지만, 실제 앱에서는 이 값이 SharedPreferences, Hive, SQLite에서 불러온 결과가 됩니다.

var todos = const [
  Todo(title: 'Flutter 문서 읽기'),
  Todo(title: '운동 기록하기', done: true),
];

생성 Create

saveTodo에서 editingIndex가 -1이면 새 할 일을 추가합니다. 입력값이 비어 있으면 아무 작업도 하지 않으며, 저장 후에는 입력창을 비워 다음 입력을 받을 준비를 합니다.

if (editingIndex < 0) {
  todos = [...todos, Todo(title: text)];
  controller.clear();
}

조회 Read

List.generate는 todos의 현재 길이만큼 ListTile을 만듭니다. 이 부분이 CRUD의 조회 화면입니다. 데이터가 추가, 수정, 삭제될 때 setState가 호출되면 List.generate가 다시 실행되어 최신 목록을 보여줍니다.

List.generate(todos.length, (index) {
  final todo = todos[index];
  return ListTile(title: Text(todo.title));
});

수정 Update

수정 버튼을 누르면 startEdit가 실행되고 editingIndex에 수정할 항목 번호를 저장합니다. 이후 저장 버튼을 누르면 새 Todo를 만드는 대신 해당 index의 Todo만 copyWith로 교체합니다.

editingIndex = index;
controller.text = todos[index].title;
todos = [for (var i = 0; i < todos.length; i++) i == editingIndex ? todos[i].copyWith(title: text) : todos[i]];

완료 상태 변경

완료 체크도 수정 작업의 한 종류입니다. toggleTodo는 선택한 항목의 done 값만 반대로 바꾼 새 Todo를 만들고, 나머지 항목은 그대로 둡니다.

todos = [
  for (var i = 0; i < todos.length; i++)
    i == index ? todos[i].copyWith(done: !todos[i].done) : todos[i],
];

삭제 Delete

removeTodo는 선택한 index의 항목을 목록에서 제거합니다. 실제 로컬 저장소를 붙이면 이 함수 안에서 메모리 목록을 바꾼 뒤 저장소에도 같은 변경을 반영합니다.

void removeTodo(int index) {
  setState(() => todos = [...todos]..removeAt(index));
}

로컬 저장 확장

예제는 메모리 목록으로 시작하지만 CRUD 함수의 경계가 분리되어 있기 때문에 저장소를 붙이기 쉽습니다. saveTodo, toggleTodo, removeTodo 끝에서 saveTodos(todos)를 호출하면 SharedPreferences나 Hive 저장 방식으로 확장할 수 있습니다.

await todoRepository.saveAll(todos);
final todos = await todoRepository.findAll();