Skip to main content

One post tagged with "Todo App"

View All Tags

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

· 9 min read

Flutter Provider Todo App

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 rebuild
  • context.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 Future cho 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:

  1. ✅ Tạo model cho Todo
  2. ✅ Sử dụng Provider để quản lý state
  3. ✅ Tích hợp SharedPreferences để lưu trữ
  4. ✅ Xây dựng UI với Consumer
  5. ✅ 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!