Hướng dẫn sử dụng Provider để xây dựng app Todo

Giới thiệu
Trong bài viết này, chúng ta sẽ xây dựng một ứng dụng Todo hoàn chỉnh sử dụng Provider để quản lý state. Đây là một dự án thực tế giúp bạn hiểu cách sử dụng Provider trong Flutter một cách hiệu quả.
Yêu cầu dự án
Ứng dụng Todo của chúng ta sẽ có các tính năng:
- ✅ Thêm task mới
- ✅ Xóa task
- ✅ Đánh dấu task hoàn thành
- ✅ Lọc task (Tất cả, Đang làm, Hoàn thành)
- ✅ Lưu trữ dữ liệu local
Bước 1: Setup dự án
Tạo Flutter project
flutter create todo_provider_app
cd todo_provider_app
Thêm dependencies
Thêm vào file pubspec.yaml:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1
shared_preferences: ^2.2.2
uuid: ^4.2.1
Chạy lệnh:
flutter pub get
Bước 2: Tạo Model
Tạo file lib/models/todo.dart:
class Todo {
final String id;
final String title;
final bool isCompleted;
final DateTime createdAt;
Todo({
required this.id,
required this.title,
this.isCompleted = false,
required this.createdAt,
});
Todo copyWith({
String? id,
String? title,
bool? isCompleted,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isCompleted': isCompleted,
'createdAt': createdAt.toIso8601String(),
};
}
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'],
title: json['title'],
isCompleted: json['isCompleted'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
);
}
}
Bước 3: Tạo Provider
Tạo file lib/providers/todo_provider.dart:
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../models/todo.dart';
import 'package:uuid/uuid.dart';
enum FilterType { all, active, completed }
class TodoProvider with ChangeNotifier {
List<Todo> _todos = [];
FilterType _filter = FilterType.all;
List<Todo> get todos {
switch (_filter) {
case FilterType.active:
return _todos.where((todo) => !todo.isCompleted).toList();
case FilterType.completed:
return _todos.where((todo) => todo.isCompleted).toList();
default:
return _todos;
}
}
FilterType get filter => _filter;
int get activeTodosCount => _todos.where((todo) => !todo.isCompleted).length;
int get completedTodosCount => _todos.where((todo) => todo.isCompleted).length;
TodoProvider() {
_loadTodos();
}
// Load todos from SharedPreferences
Future<void> _loadTodos() async {
try {
final prefs = await SharedPreferences.getInstance();
final todosJson = prefs.getStringList('todos') ?? [];
_todos = todosJson
.map((json) => Todo.fromJson(jsonDecode(json)))
.toList();
notifyListeners();
} catch (e) {
debugPrint('Error loading todos: $e');
}
}
// Save todos to SharedPreferences
Future<void> _saveTodos() async {
try {
final prefs = await SharedPreferences.getInstance();
final todosJson = _todos
.map((todo) => jsonEncode(todo.toJson()))
.toList();
await prefs.setStringList('todos', todosJson);
} catch (e) {
debugPrint('Error saving todos: $e');
}
}
// Add new todo
Future<void> addTodo(String title) async {
if (title.trim().isEmpty) return;
final newTodo = Todo(
id: const Uuid().v4(),
title: title.trim(),
createdAt: DateTime.now(),
);
_todos.add(newTodo);
notifyListeners();
await _saveTodos();
}
// Toggle todo completion
Future<void> toggleTodo(String id) async {
final index = _todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
_todos[index] = _todos[index].copyWith(
isCompleted: !_todos[index].isCompleted,
);
notifyListeners();
await _saveTodos();
}
}
// Delete todo
Future<void> deleteTodo(String id) async {
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
await _saveTodos();
}
// Clear completed todos
Future<void> clearCompleted() async {
_todos.removeWhere((todo) => todo.isCompleted);
notifyListeners();
await _saveTodos();
}
// Set filter
void setFilter(FilterType filter) {
_filter = filter;
notifyListeners();
}
}
Bước 4: Tạo UI Components
Todo Item Widget
Tạo file lib/widgets/todo_item.dart:
import 'package:flutter/material.dart';
import '../models/todo.dart';
class TodoItem extends StatelessWidget {
final Todo todo;
final VoidCallback onToggle;
final VoidCallback onDelete;
const TodoItem({
Key? key,
required this.todo,
required this.onToggle,
required this.onDelete,
}) : super(key: key);
Widget build(BuildContext context) {
return Dismissible(
key: Key(todo.id),
direction: DismissDirection.endToStart,
onDismissed: (_) => onDelete(),
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
color: Colors.red,
child: const Icon(Icons.delete, color: Colors.white),
),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => onToggle(),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
color: todo.isCompleted
? Colors.grey
: Colors.black,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onDelete,
color: Colors.red,
),
),
);
}
}
Add Todo Dialog
Tạo file lib/widgets/add_todo_dialog.dart:
import 'package:flutter/material.dart';
class AddTodoDialog extends StatefulWidget {
const AddTodoDialog({Key? key}) : super(key: key);
State<AddTodoDialog> createState() => _AddTodoDialogState();
}
class _AddTodoDialogState extends State<AddTodoDialog> {
final _controller = TextEditingController();
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Thêm Task Mới'),
content: TextField(
controller: _controller,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Nhập task của bạn...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _submit(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Hủy'),
),
ElevatedButton(
onPressed: _submit,
child: const Text('Thêm'),
),
],
);
}
void _submit() {
if (_controller.text.trim().isNotEmpty) {
Navigator.of(context).pop(_controller.text.trim());
}
}
}
Bước 5: Tạo Main Screen
Tạo file lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_provider.dart';
import '../widgets/todo_item.dart';
import '../widgets/add_todo_dialog.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo App'),
actions: [
Consumer<TodoProvider>(
builder: (context, provider, _) {
if (provider.completedTodosCount > 0) {
return IconButton(
icon: const Icon(Icons.delete_sweep),
tooltip: 'Xóa task đã hoàn thành',
onPressed: () async {
await provider.clearCompleted();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xóa các task hoàn thành'),
),
);
}
},
);
}
return const SizedBox.shrink();
},
),
],
),
body: Column(
children: [
// Filter buttons
Consumer<TodoProvider>(
builder: (context, provider, _) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFilterButton(
context,
'Tất cả',
FilterType.all,
provider.filter == FilterType.all,
() => provider.setFilter(FilterType.all),
),
_buildFilterButton(
context,
'Đang làm',
FilterType.active,
provider.filter == FilterType.active,
() => provider.setFilter(FilterType.active),
),
_buildFilterButton(
context,
'Hoàn thành',
FilterType.completed,
provider.filter == FilterType.completed,
() => provider.setFilter(FilterType.completed),
),
],
),
);
},
),
// Stats
Consumer<TodoProvider>(
builder: (context, provider, _) {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatCard(
context,
'Tổng',
provider.todos.length.toString(),
Colors.blue,
),
_buildStatCard(
context,
'Đang làm',
provider.activeTodosCount.toString(),
Colors.orange,
),
_buildStatCard(
context,
'Hoàn thành',
provider.completedTodosCount.toString(),
Colors.green,
),
],
),
);
},
),
// Todo list
Expanded(
child: Consumer<TodoProvider>(
builder: (context, provider, _) {
if (provider.todos.isEmpty) {
return const Center(
child: Text(
'Chưa có task nào.\nThêm task mới để bắt đầu!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
);
}
return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(
todo: todo,
onToggle: () => provider.toggleTodo(todo.id),
onDelete: () => provider.deleteTodo(todo.id),
);
},
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await showDialog<String>(
context: context,
builder: (_) => const AddTodoDialog(),
);
if (result != null && context.mounted) {
await context.read<TodoProvider>().addTodo(result);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã thêm task mới'),
duration: Duration(seconds: 1),
),
);
}
},
child: const Icon(Icons.add),
),
);
}
Widget _buildFilterButton(
BuildContext context,
String label,
FilterType type,
bool isSelected,
VoidCallback onTap,
) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}
Widget _buildStatCard(
BuildContext context,
String label,
String value,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
);
}
}
Bước 6: Setup Provider trong main.dart
Cập nhật file lib/main.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/todo_provider.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TodoProvider(),
child: MaterialApp(
title: 'Todo App',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomeScreen(),
debugShowCheckedModeBanner: false,
),
);
}
}
Bước 7: Chạy ứng dụng
flutter run
Giải thích các khái niệm quan trọng
1. ChangeNotifier
TodoProvider extends ChangeNotifier, cho phép nó thông báo cho các listeners khi state thay đổi.
2. notifyListeners()
Mỗi khi state thay đổi, gọi notifyListeners() để UI tự động rebuild.
3. Consumer Widget
Consumer<TodoProvider> chỉ rebuild phần UI cần thiết khi state thay đổi, tối ưu hiệu suất.
4. context.read() vs context.watch()
context.read<TodoProvider>(): Dùng khi chỉ cần gọi method, không cần rebuildcontext.watch<TodoProvider>(): Dùng khi cần rebuild khi state thay đổi
Best Practices
1. Tách biệt logic và UI
- Provider chứa business logic
- Widget chỉ hiển thị UI
2. Sử dụng Consumer đúng cách
- Chỉ wrap phần UI cần rebuild
- Tránh wrap toàn bộ widget tree
3. Xử lý async operations
- Sử dụng
Futurecho async operations - Hiển thị loading state khi cần
4. Error handling
- Luôn xử lý lỗi trong async operations
- Hiển thị thông báo lỗi cho user
Mở rộng ứng dụng
Thêm tính năng chỉnh sửa task
Future<void> updateTodo(String id, String newTitle) async {
final index = _todos.indexWhere((todo) => todo.id == id);
if (index != -1) {
_todos[index] = _todos[index].copyWith(title: newTitle);
notifyListeners();
await _saveTodos();
}
}
Thêm priority cho task
enum Priority { low, medium, high }
class Todo {
// ... existing code
final Priority priority;
// ... rest of code
}
Thêm due date
class Todo {
// ... existing code
final DateTime? dueDate;
// ... rest of code
}
Kết luận
Trong bài viết này, chúng ta đã xây dựng một ứng dụng Todo hoàn chỉnh sử dụng Provider. Các điểm chính:
- ✅ Tạo model cho Todo
- ✅ Sử dụng Provider để quản lý state
- ✅ Tích hợp SharedPreferences để lưu trữ
- ✅ Xây dựng UI với Consumer
- ✅ Xử lý các thao tác CRUD
Provider là một giải pháp tuyệt vời cho quản lý state trong Flutter, đặc biệt phù hợp với các ứng dụng nhỏ đến trung bình. Hãy thử nghiệm và mở rộng ứng dụng này để học thêm về Provider!
Happy coding với Flutter và Provider!
