2025. 3. 17. 13:00ㆍ같이 공부합시다 - Flutter/Dart & Flutter 기초부터 실전까지
Flutter에서 할 일 목록(Todo List) 관리 기능을 구현하며, Riverpod을 활용한 CRUD(Create, Read, Update, Delete) 상태 관리
StateNotifierProvider를 사용하여 리스트 형태의 데이터를 관리하고, UI에서 추가, 수정, 삭제 기능을 적용



🔔 주제
🔸 Riverpod을 활용한 CRUD 상태 관리
🔸 StateNotifierProvider를 사용하여 리스트 데이터 관리
🔸 할 일 목록 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 구현
🔸 UI에서 데이터를 반영하고 리스트를 동적으로 업데이트
1️⃣ Riverpod을 활용한 CRUD 상태 관리
🔸 CRUD란?
CRUD는 가장 기본적인 데이터 처리 방식입니다.
🔹Create(생성) → 새로운 데이터를 추가
🔹Read(조회) → 저장된 데이터를 가져오기
🔹Update(수정) → 기존 데이터를 변경
🔹Delete(삭제) → 데이터를 제거
🔸Riverpod을 활용하는 이유
Flutter에서 여러 화면에서 데이터를 공유하거나 상태를 관리하려면 Provider 또는 Riverpod을 사용해야 합니다.
Riverpod을 사용하면 더 직관적이고 안전한 방식으로 데이터를 관리할 수 있습니다.
✅ 구현할 기능:
- 할 일 추가(Create) → 새로운 할 일을 목록에 추가
- 할 일 조회(Read) → 현재 할 일 목록을 UI에 표시
- 할 일 수정(Update) → 특정 할 일의 내용을 변경
- 할 일 삭제(Delete) → 완료된 할 일을 목록에서 제거
2️⃣ 프로젝트 설정 및 Riverpod 패키지 설치
✅ Riverpod을 사용하려면 패키지를 설치해야 합니다.
📌 터미널에서 아래 명령어 실행
flutter pub add flutter_riverpod
📌 설치 완료 후 pubspec.yaml에 아래 내용이 추가되었는지 확인
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.6.1 # 2025-03-12 기준 최신 버전
📌 프로젝트 구조
lib/
│── providers/todo_provider.dart # Riverpod을 활용한 상태 관리
│── screens/todo_screen.dart # UI 및 사용자 입력 처리
│── main.dart
3️⃣ main.dart 코드
✅ Flutter에서 Riverpod을 사용하여 할 일 목록(Todo List)을 관리하는 main.dart 코드입니다.
✅ ProviderScope를 설정하여 todoProvider를 사용할 수 있도록 구성합니다.
✅ 앱의 첫 화면을 TodoScreen으로 설정합니다.
📌 main.dart 코드
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'screens/todo_screen.dart';
void main() {
runApp(const ProviderScope(child: MyApp())); // ✅ Riverpod 사용을 위한 ProviderScope 추가
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // 디버그 배너 제거
title: 'SteadyBuilder Todo App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(useMaterial3: true, seedColor: Colors.amber), // 앱 기본 테마 색상 설정
),
home: const TodoScreen(), // ✅ 첫 화면을 TodoScreen으로 설정
);
}
}
🔔 코드 설명
✅ ProviderScope(child: MyApp())
- ProviderScope는 Riverpod을 사용하기 위해 반드시 필요한 설정입니다.
- 모든 Provider(예: todoProvider)를 앱 전체에서 사용할 수 있도록 전역 상태를 관리합니다.
- 이 설정이 없으면 ref.watch()나 ref.read()를 사용할 수 없습니다.
4️⃣ StateNotifierProvider를 사용하여 리스트 데이터 관리
1. Todo 모델 만들기
할 일 목록을 관리하려면, 할 일(Todo) 데이터를 저장하는 모델이 필요합니다.
📌 todo_provider.dart 에 모델 추가
class Todo {
final String id; // 각 할 일의 고유 ID
final String title; // 할 일의 제목
bool isDone; // 완료 여부
Todo({required this.id, required this.title, this.isDone = false});
}
✅ id → 할 일을 식별하기 위한 고유값 (DateTime을 활용)
✅ title → 사용자가 입력한 할 일 제목
✅ isDone → 할 일이 완료되었는지 여부 (체크박스 관리)
💡 id가 필요한 이유
🔸 같은 제목을 가진 할 일이 여러 개 있을 수 있습니다.
🔸id 없이 특정 할 일을 수정하거나 삭제할 경우, 동일한 제목을 가진 모든 할 일이 변경될 수 있습니다.
🔸예 :
Todo(title: "운동하기"); // 1번 항목
Todo(title: "운동하기"); // 2번 항목
→ 어떤 '운동하기'를 삭제해야 할까? id 없이는 구분 불가.
🔸 이와 같이 정확한 항목을 특정할 수 있으므로 수정(업데이트)하거나 삭제할 때 id 값을 사용합니다.
2. 할 일 목록을 관리하는 StateNotifier 구현
Riverpod에서는 StateNotifier를 사용하여 상태를 관리합니다.
여기서는 StateNotifier<List<Todo>>를 사용하여 할 일 목록을 관리합니다.
📌 todo_provider.dart 에 StateNotifier 추가
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 할 일 데이터 모델
class Todo {
final String id; // 각 할 일의 고유 ID
final String title; // 할 일의 제목
bool isDone; // 완료 여부
Todo({required this.id, required this.title, this.isDone = false});
}
// StateNotifier를 사용한 Todo 리스트 관리
class TodoNotifier extends StateNotifier<List<Todo>> {
TodoNotifier() : super([]);
// 할 일 추가
void addTodo(String title) {
final newTodo = Todo(id: DateTime.now().toString(), title: title);
state = [...state, newTodo];
}
// 할 일 상태 업데이트 (완료 처리)
void toggleTodo(String id) {
state = state.map((todo) {
if (todo.id == id) {
return Todo(id: todo.id, title: todo.title, isDone: !todo.isDone);
}
return todo;
}).toList();
}
// 할 일 삭제
void deleteTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
// StateNotifierProvider로 상태 등록
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
return TodoNotifier();
});
🔔 코드 설명
✅ Todo 클래스 → 할 일의 ID, 제목, 완료 여부를 저장하는 데이터 모델
✅ TodoNotifier → StateNotifier<List<Todo>>를 상속받아 할 일 목록을 관리
✅ addTodo() → 새로운 할 일을 추가
✅ toggleTodo() → 완료 여부를 변경
✅ deleteTodo() → 할 일을 삭제
✅ todoProvider → Riverpod의 StateNotifierProvider를 사용하여 상태를 등록
🔥 class TodoNotifier 코드 상세 설명
🔔StateNotifier<List<Todo>>란?
class TodoNotifier extends StateNotifier<List<Todo>> {
✅ StateNotifier<T>는 Riverpod에서 상태를 관리하기 위한 클래스입니다.
✅ 여기서 T는 상태로 사용할 데이터 타입을 의미합니다.
✅ StateNotifier<List<Todo>>이므로, 할 일 목록을 관리하는 역할을 하게 됩니다.
📌super([])는 무엇을 의미할까?
TodoNotifier() : super([]);
✅ super([])는 초기 상태를 빈 리스트([])로 설정하는 코드입니다.
✅ 즉, TodoNotifier가 처음 생성될 때 할 일 목록이 없는 상태로 시작합니다.
📌 StateNotifier를 사용하는 이유
✅ Flutter에서 상태를 관리하는 방법 중 하나
✅ 기존의 setState()를 대체하여 상태를 더 효율적으로 관리
✅ 앱의 여러 부분에서 같은 상태를 공유할 수 있도록 함
🔔 addTodo(String title) 코드 상세 설명
이 함수는 사용자가 새로운 할 일을 입력하면 목록에 추가하는 역할을 합니다.
사용자가 할 일을 입력하고 추가 버튼을 클릭하면 새로운 Todo 객체가 생성되어 리스트에 추가됩니다.
📌 함수 전체 코드
void addTodo(String title) {
final newTodo = Todo(id: DateTime.now().toString(), title: title);
state = [...state, newTodo];
}
1️⃣ final newTodo = Todo(...)
final newTodo = Todo(id: DateTime.now().toString(), title: title);
✅ Todo 모델을 기반으로 새로운 할 일 객체를 만듭니다.
✅ id: DateTime.now().toString() → 현재 시간을 문자열로 변환하여 고유한 ID 생성
✅ title: title → 사용자가 입력한 제목을 저장
✅ isDone 값은 기본값 false (미완료 상태)로 설정됨
💡 예제:
사용자가 "책 읽기"를 입력하면 다음과 같은 Todo 객체가 생성됩니다.
Todo(id: "2024-03-12 14:30:45.123456", title: "책 읽기", isDone: false)
2️⃣ state = [...state, newTodo];
state = [...state, newTodo];
✅ state는 현재 할 일 목록을 저장하는 리스트입니다.
✅ 기존 할 일 리스트에 새로운 newTodo를 추가하는 코드입니다.
✅ 여기서 newTodo 는 방금 전에 final 로 만든 새로운 Todo 입니다.
💡 설명:
- state는 Riverpod의 StateNotifier에서 관리하는 상태값 ⇒ 할 일 목록 리스트(List) 입니다.
- ...state는 기존 리스트의 모든 항목을 복사하여 유지합니다.
- [...state, newTodo] → 기존 리스트를 유지하면서 newTodo를 추가한 새로운 리스트를 만든다는 의미입니다.
📢 중요: state를 직접 수정하는 것이 아니라, 새로운 리스트를 만들어서 state에 할당해야 합니다. (Flutter에서는 상태를 불변(immutable)하게 유지하는 것이 중요합니다.)
3️⃣ 동작 방식
✅ 기존 할 일 목록:
[
Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: false),
Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
]
✅ 사용자가 "Flutter 공부하기" 추가 → addTodo("Flutter 공부하기") 실행 후 상태 변경:
[
Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: false),
Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
Todo(id: "2024-03-12 14:34:21.123456", title: "Flutter 공부하기", isDone: false), // 추가됨!
]
✅ 새로운 할 일이 기존 리스트에 추가되었고, 화면이 업데이트됩니다.
🔔 toggleTodo(String id) 코드 상세 설명
✅ 이 함수는 특정 id를 가진 할 일(Todo)의 완료 상태를 변경하는 역할을 합니다.
✅ 즉, 사용자가 체크박스를 클릭하면 isDone 값이 true → false 또는 false → true로 변경됩니다.
📌 함수 전체 코드
void toggleTodo(String id) {
state = state.map((todo) {
if (todo.id == id) {
return Todo(id: todo.id, title: todo.title, isDone: !todo.isDone);
}
return todo;
}).toList();
}
✅ state.map((todo) { ... }).toList();
- map() 함수를 사용하여 리스트의 모든 항목을 검사합니다.
- id가 일치하는 항목만 상태를 변경하고, 나머지는 그대로 둡니다.
- 최종적으로 .toList();를 사용하여 새로운 리스트로 변환하여 state에 저장합니다.
- ⭐ Flutter에서는 기존 리스트를 직접 수정하지 않고, 항상 새로운 리스트를 만들어 state에 할당해야 합니다.
1️⃣ state.map((todo) { ... }).toList();
state = state.map((todo) {
- 기존 할 일 리스트(state)의 각 항목을 검사합니다.
- todo는 현재 리스트에서 가져온 **각 할 일 객체(Todo)**를 의미합니다.
2️⃣ 특정 id와 일치하는 항목 찾기
if (todo.id == id) {
- 사용자가 클릭한 할 일의 id와 현재 todo의 id를 비교합니다.
- id가 일치하는 경우, 완료 상태를 변경해야 합니다.
3️⃣ 새로운 Todo 객체를 생성하여 완료 상태 변경
return Todo(id: todo.id, title: todo.title, isDone: !todo.isDone);
- 새로운 Todo 객체를 생성합니다.
- isDone: !todo.isDone
- true → false,
- false → true
- 즉, 체크박스를 클릭할 때마다 상태가 반전됩니다.
4️⃣ 기존 할 일 유지
return todo;
- id가 일치하지 않는 경우 기존 할 일을 그대로 반환합니다.
5️⃣ 최종적으로 toList()를 사용하여 새로운 리스트 생성
}).toList();
- map() 함수는 리스트가 아니라 Iterable(반복 가능한 데이터 구조)을 반환합니다.
- .toList()를 사용하여 새로운 리스트(List)로 변환한 후 state에 저장합니다.
6️⃣ 동작 방식 (예제)
✅ 초기 할 일 목록 (state)
[
Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: false),
Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
]
✅ 사용자가 "운동하기"(id: “2024-03-12 14:30:45.123456")의 체크박스를 클릭
✅ toggleTodo("2024-03-12 14:30:45.123456") 실행 후 상태 변화
[
Todo(id: "2024-03-12 14:30:45.123456", title: "운동하기", isDone: true), // ✅ 변경됨
Todo(id: "2024-03-12 14:32:48.123456", title: "책 읽기", isDone: false),
]
✅ 다시 "운동하기" 체크박스를 클릭하면 isDone이 false로 변경됨
🔔 deleteTodo(String id) 코드 상세 설명
✅ 이 함수는 특정 id를 가진 할 일(Todo)을 목록에서 삭제하는 역할을 합니다.
✅ 즉, 사용자가 삭제 버튼(🗑️)을 클릭하면 해당 항목이 리스트에서 제거됩니다.
📌 함수 전체 코드
void deleteTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
✅ state.where((todo) => todo.id != id).toList();
- where() 함수를 사용하여 리스트에서 특정 조건을 만족하는 항목만 유지합니다.
- todo.id != id → 삭제하려는 id와 일치하지 않는 항목만 남깁니다.
- 최종적으로 .toList();를 사용하여 새로운 리스트로 변환하여 state에 저장합니다.
- ⭐ Flutter에서는 기존 리스트를 직접 수정하지 않고, 항상 새로운 리스트를 만들어 state에 할당해야 합니다. (중요하기 때문에 계속 반복 강조)
1️⃣ 특정 id를 가진 항목을 제외한 새로운 리스트 생성
state = state.where((todo) => todo.id != id).toList();
- where() 함수는 주어진 조건을 만족하는 항목만 남겨 새로운 리스트를 반환하는 함수입니다.
- todo.id != id → 삭제할 항목의 id와 일치하지 않는 항목들만 남깁니다.
💡 예제:
✅ 초기 할 일 목록 (state)
⚠️ id 가 너무 길어서 예제에서만 1, 2, 3 으로 대체합니다.
[
Todo(id: "1", title: "운동하기", isDone: false),
Todo(id: "2", title: "책 읽기", isDone: false),
Todo(id: "3", title: "Flutter 공부", isDone: true),
]
✅ 사용자가 "책 읽기"(id: "2") 삭제 버튼 클릭 → deleteTodo("2") 실행 후 상태 변화
[
Todo(id: "1", title: "운동하기", isDone: false),
Todo(id: "3", title: "Flutter 공부", isDone: true),
]
✅ id가 "2"인 항목이 삭제됨
2️⃣ 기존 방식(for 루프)과의 차이점
✅ 일반적인 for 루프를 사용한 삭제 방법
void deleteTodo(String id) {
List<Todo> newList = [];
for (var todo in state) {
if (todo.id != id) {
newList.add(todo);
}
}
state = newList;
}
✔️ where()을 사용하면 위와 같은 for 루프 없이 더 간결하게 삭제 가능
✔️ 성능상 where()가 더 효율적이며, 유지보수가 쉬움
🔔 StateNotifierProvider로 상태 등록하는 코드 상세 설명
✅ 이 코드는 Riverpod에서 할 일 목록(Todo)의 상태를 관리하기 위해 StateNotifierProvider를 설정하는 부분입니다.
✅ 즉, 앱 내에서 todoProvider를 사용하면 TodoNotifier가 관리하는 상태(할 일 목록)를 접근하고 변경할 수 있습니다.
📌 해당 코드 분석
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
return TodoNotifier();
});
✅ StateNotifierProvider<TodoNotifier, List<Todo>>
- StateNotifierProvider는 Flutter Riverpod에서 상태(State)를 전역적으로 관리하는 방법 중 하나입니다.
- <TodoNotifier, List<Todo>> → TodoNotifier 클래스가 List<Todo> 타입의 상태를 관리한다는 의미입니다.
✅ (ref) { return TodoNotifier(); }
- ref는 Provider 내부에서 다른 Provider를 참조할 수 있도록 해주는 객체입니다.
- return TodoNotifier(); → TodoNotifier를 생성하여 상태 관리를 시작합니다.
1️⃣ final todoProvider
final todoProvider = ...
- todoProvider는 전역적으로 상태를 관리하는 Provider입니다.
- 앱의 어느 곳에서든 todoProvider를 사용하여 할 일 목록을 불러오거나 업데이트할 수 있습니다.
2️⃣ StateNotifierProvider<TodoNotifier, List<Todo>>
StateNotifierProvider<TodoNotifier, List<Todo>>
- StateNotifierProvider를 사용하면 Riverpod이 StateNotifier 기반 상태를 자동으로 관리해 줍니다.
- <TodoNotifier, List<Todo>>
- TodoNotifier → 할 일 목록을 관리하는 클래스 (상태 로직 포함)
- List<Todo> → TodoNotifier가 관리하는 상태의 데이터 타입
3️⃣ (ref) { return TodoNotifier(); }
(ref) {
return TodoNotifier();
}
- ref: Riverpod에서 다른 Provider를 참조할 때 사용되는 객체
- return TodoNotifier(); → TodoNotifier 인스턴스를 생성하여 todoProvider로 등록
📌 todoProvider가 동작하는 방식 (전체 흐름)
1️⃣ 사용자가 할 일을 추가하면
- todoProvider.notifier.addTodo("책 읽기") 실행
- TodoNotifier의 addTodo()가 호출됨
- 새로운 Todo 객체가 state에 추가됨
- Riverpod이 상태 변경을 감지하여 UI를 자동 업데이트
2️⃣ 사용자가 할 일을 삭제하면
- todoProvider.notifier.deleteTodo(id) 실행
- TodoNotifier의 deleteTodo()가 호출됨
- id가 일치하는 항목이 리스트에서 제거됨
- 상태가 변경되면서 UI가 자동으로 업데이트됨
3️⃣ 사용자가 할 일을 완료하면
- todoProvider.notifier.toggleTodo(id) 실행
- TodoNotifier의 toggleTodo()가 호출됨
- 특정 id를 가진 Todo의 isDone 값이 true ↔ false로 변경됨
- UI가 즉시 변경됨
4️⃣ UI에서 할 일 목록 관리 (CRUD 기능 구현)
✅ 할 일 목록을 UI에 표시하고, 사용자가 입력한 데이터를 반영합니다.
✅ 추가, 수정, 삭제 버튼을 눌러 데이터를 변경할 수 있습니다.
📌 todo_screen.dart UI 코드 작성
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/todo_provider.dart';
class TodoScreen extends ConsumerWidget {
const TodoScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todoList = ref.watch(todoProvider);
final todoNotifier = ref.read(todoProvider.notifier);
final TextEditingController controller = TextEditingController();
return Scaffold(
appBar: AppBar(title: const Text("Todo List (Riverpod)")),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
children: [
// TextField
Expanded(
child: TextField(
controller: controller,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "할 일 입력",
),
),
),
const SizedBox(width: 10),
// ElevatedButton
ElevatedButton(
onPressed: () {
if (controller.text.isNotEmpty) {
todoNotifier.addTodo(controller.text);
}
controller.clear();
},
child: const Text("추가"),
),
],
),
),
const SizedBox(height: 20),
// ListView.builder
Expanded(
child: ListView.builder(
itemCount: todoList.length,
itemBuilder: (context, index) {
final todo = todoList[index];
return Card(
child: ListTile(
title: Text(
todo.title,
style: TextStyle(
decoration:
todo.isDone ? TextDecoration.lineThrough : null,
),
),
leading: Checkbox(
value: todo.isDone,
onChanged: (value) {
todoNotifier.toggleTodo(todo.id);
},
),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
todoNotifier.deleteTodo(todo.id);
},
),
),
);
},
),
),
],
),
);
}
}
🔔 코드 설명
✅ TextField → 사용자가 할 일 입력
✅ IconButton (추가) → todoNotifier.addTodo(controller.text) 실행하여 새 할 일 추가
✅ ListView.builder → todoList를 불러와 화면에 표시
✅ Checkbox → 체크박스를 클릭하면 toggleTodo(todo.id) 실행하여 완료 처리
✅ IconButton (삭제) → todoNotifier.deleteTodo(todo.id) 실행하여 삭제
📌 코드 요약
✅ todo_provider.dart
🔸Todo 모델 만들기 (id, title, isDone)
🔸**class TodoNotifier 만들기 (상속 : StateNotifier<List<Todo>>)**
🔸**addTodo(String title) 함수 만들기**
🔸**toggleTodo(String id) 함수 만들기 (map() 사용)**
🔸**deleteTodo(String id) 함수 만들기 (where() 사용)**
🔸**StateNotifierProvider로 상태 등록하기**
✅ todo_screen.dart
🔸class TodoScreen 만들기 (상속 : ConsumerWidget)
🔸provider 구독 , 참조하는 변수 선언 (ref.watch , ref.read)
🔸할 일 추가 UI 섹션 만들기 (입력 , 추가)
🔸할 일 목록 UI 섹션 만들기 (리스트 , 완료체크박스 , 삭제버튼)
📌 내용 요약
✅ Riverpod의 StateNotifierProvider를 활용하여 CRUD 기능을 구현할 수 있다.
✅ 할 일 목록을 리스트 형태로 관리하며, add, update, delete 메서드를 활용할 수 있다.
✅ ref.watch()를 사용하여 상태를 UI에 반영하고, ref.read()를 통해 상태를 변경할 수 있다.
✅ UI에서 사용자의 입력을 받아 StateNotifier를 통해 상태를 업데이트할 수 있다.
'같이 공부합시다 - Flutter > Dart & Flutter 기초부터 실전까지' 카테고리의 다른 글
| 📌 Day 17: Dio + Riverpod 완벽 활용 가이드! (Flutter API 통신 실전 패턴 총정리) (2) | 2025.03.26 |
|---|---|
| 📌 Day 16: Flutter HTTP 패키지를 활용한 API 요청 (GET, POST, Json 데이터 처리, FutureProvider< (1) | 2025.03.19 |
| 📌 Day 14: Riverpod 상태 관리 패턴 (Provider와 비교하며 배우는 실전 적용법) (1) | 2025.03.07 |
| 📌 Day 13: Provider 상태 관리 적용 테스트 (Counter 앱, 다크모드까지) (1) | 2025.03.06 |
| 📌 Day 12: Provider 패턴 적용 (ChangeNotifier, Consumer) (0) | 2025.03.05 |