Skip to main content

3 posts tagged with "Provider"

View All Tags

Flutter: Giới thiệu về Provider package

· 5 min read

Provider là một trong những package phổ biến nhất để quản lý state trong Flutter. Bài viết này sẽ giới thiệu về Provider và cách sử dụng nó hiệu quả.

Provider Package

1. Giới thiệu về Provider

Provider là một giải pháp quản lý state được phát triển bởi Remi Rousselet, một trong những core team member của Flutter. Provider được thiết kế để đơn giản hóa việc quản lý state và dependency injection trong ứng dụng Flutter.

1.1. Cài đặt Provider

Thêm Provider vào file pubspec.yaml:

dependencies:
provider: ^6.0.0

1.2. Các loại Provider

Provider cung cấp nhiều loại provider khác nhau:

  • Provider: Cung cấp một giá trị
  • ChangeNotifierProvider: Cung cấp một ChangeNotifier
  • ListenableProvider: Cung cấp một Listenable
  • ValueListenableProvider: Cung cấp một ValueListenable
  • StreamProvider: Cung cấp một Stream
  • FutureProvider: Cung cấp một Future

2. Sử dụng Provider

2.1. Tạo Provider

class CounterProvider extends ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners();
}

void decrement() {
_count--;
notifyListeners();
}
}

2.2. Cung cấp Provider

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

2.3. Sử dụng Provider

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'Count: ${counter.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
ElevatedButton(
onPressed: () {
context.read<CounterProvider>().increment();
},
child: Text('Increment'),
),
],
),
),
);
}
}

3. Các Pattern phổ biến

3.1. MultiProvider

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}

3.2. ProxyProvider

class UserProvider extends ChangeNotifier {
String _name = '';
String get name => _name;

void updateName(String name) {
_name = name;
notifyListeners();
}
}

class UserProfileProvider extends ChangeNotifier {
final UserProvider userProvider;
String _greeting = '';

UserProfileProvider(this.userProvider) {
userProvider.addListener(_updateGreeting);
_updateGreeting();
}

String get greeting => _greeting;

void _updateGreeting() {
_greeting = 'Hello, ${userProvider.name}!';
notifyListeners();
}

@override
void dispose() {
userProvider.removeListener(_updateGreeting);
super.dispose();
}
}

// Sử dụng
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserProvider()),
ChangeNotifierProxyProvider<UserProvider, UserProfileProvider>(
create: (context) => UserProfileProvider(
Provider.of<UserProvider>(context, listen: false),
),
update: (context, userProvider, previous) =>
UserProfileProvider(userProvider),
),
],
child: MyApp(),
)

4. Best Practices

4.1. Tối ưu hóa hiệu suất

// Sử dụng Consumer thay vì Provider.of
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Column(
children: [
Text('Count: ${counter.count}'),
child!, // Widget không thay đổi
],
);
},
child: const StaticWidget(), // Widget được tạo một lần
)

// Sử dụng Selector để chỉ rebuild khi cần
Selector<CounterProvider, int>(
selector: (_, provider) => provider.count,
builder: (context, count, child) {
return Text('Count: $count');
},
)

4.2. Xử lý lỗi

class DataProvider extends ChangeNotifier {
String? _error;
bool _isLoading = false;
List<Data> _data = [];

String? get error => _error;
bool get isLoading => _isLoading;
List<Data> get data => _data;

Future<void> fetchData() async {
try {
_isLoading = true;
_error = null;
notifyListeners();

_data = await api.fetchData();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}

5. Ví dụ thực tế

5.1. Ứng dụng Todo

class TodoProvider extends ChangeNotifier {
List<Todo> _todos = [];
bool _isLoading = false;

List<Todo> get todos => _todos;
bool get isLoading => _isLoading;

Future<void> addTodo(Todo todo) async {
_isLoading = true;
notifyListeners();

try {
final savedTodo = await api.saveTodo(todo);
_todos.add(savedTodo);
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> toggleTodo(String id) async {
final todo = _todos.firstWhere((t) => t.id == id);
final updatedTodo = todo.copyWith(completed: !todo.completed);

try {
await api.updateTodo(updatedTodo);
final index = _todos.indexWhere((t) => t.id == id);
_todos[index] = updatedTodo;
notifyListeners();
} catch (e) {
// Xử lý lỗi
}
}
}

class TodoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: Consumer<TodoProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}

return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(
todo: todo,
onToggle: () => provider.toggleTodo(todo.id),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Hiển thị dialog thêm todo mới
},
child: Icon(Icons.add),
),
);
}
}

5.2. Ứng dụng Authentication

class AuthProvider extends ChangeNotifier {
User? _user;
bool _isLoading = false;

User? get user => _user;
bool get isLoading => _isLoading;
bool get isAuthenticated => _user != null;

Future<void> login(String email, String password) async {
_isLoading = true;
notifyListeners();

try {
_user = await authService.login(email, password);
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> logout() async {
_isLoading = true;
notifyListeners();

try {
await authService.logout();
_user = null;
} finally {
_isLoading = false;
notifyListeners();
}
}
}

class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Consumer<AuthProvider>(
builder: (context, auth, child) {
if (auth.isLoading) {
return Center(child: CircularProgressIndicator());
}

return LoginForm(
onLogin: (email, password) {
auth.login(email, password);
},
);
},
),
);
}
}

Kết luận

Provider là một giải pháp quản lý state đơn giản nhưng mạnh mẽ trong Flutter. Nó giúp bạn tổ chức code tốt hơn, dễ bảo trì và mở rộng. Việc hiểu và sử dụng Provider đúng cách sẽ giúp bạn xây dựng ứng dụng Flutter hiệu quả hơn.


Tài liệu tham khảo:

Flutter: Xây dựng chức năng quản lý Tasks

· 3 min read

Quản lý tasks là một chức năng phổ biến trong các ứng dụng quản lý công việc, Todo List, dự án... Bài viết này hướng dẫn bạn xây dựng chức năng quản lý tasks hoàn chỉnh trong Flutter, sử dụng Provider để quản lý trạng thái và lưu trữ dữ liệu.

1. Mô hình dữ liệu Task

class Task {
final String id;
String title;
String description;
DateTime? deadline;
bool isCompleted;

Task({
required this.id,
required this.title,
this.description = '',
this.deadline,
this.isCompleted = false,
});
}

2. Provider quản lý danh sách tasks

import 'package:flutter/material.dart';

class TaskProvider with ChangeNotifier {
List<Task> _tasks = [];

List<Task> get tasks => _tasks;

void addTask(Task task) {
_tasks.add(task);
notifyListeners();
}

void updateTask(Task task) {
final index = _tasks.indexWhere((t) => t.id == task.id);
if (index != -1) {
_tasks[index] = task;
notifyListeners();
}
}

void deleteTask(String id) {
_tasks.removeWhere((t) => t.id == id);
notifyListeners();
}

void toggleComplete(String id) {
final index = _tasks.indexWhere((t) => t.id == id);
if (index != -1) {
_tasks[index].isCompleted = !_tasks[index].isCompleted;
notifyListeners();
}
}
}

3. Giao diện quản lý tasks

Hiển thị danh sách tasks

Consumer<TaskProvider>(
builder: (context, taskProvider, child) {
final tasks = taskProvider.tasks;
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.title),
subtitle: Text(task.description),
trailing: Checkbox(
value: task.isCompleted,
onChanged: (_) => taskProvider.toggleComplete(task.id),
),
onTap: () {
// Chỉnh sửa task
},
onLongPress: () {
// Xóa task
taskProvider.deleteTask(task.id);
},
);
},
);
},
)

Thêm task mới

void _addTask(BuildContext context) {
final provider = Provider.of<TaskProvider>(context, listen: false);
final newTask = Task(
id: UniqueKey().toString(),
title: 'Task mới',
description: 'Mô tả...',
deadline: DateTime.now().add(Duration(days: 1)),
);
provider.addTask(newTask);
}

4. Lưu trữ tasks (local storage)

Bạn có thể sử dụng shared_preferences, hive hoặc sqflite để lưu trữ tasks. Ví dụ với shared_preferences:

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

Future<void> saveTasks(List<Task> tasks) async {
final prefs = await SharedPreferences.getInstance();
final tasksJson = jsonEncode(tasks.map((t) => {
'id': t.id,
'title': t.title,
'description': t.description,
'deadline': t.deadline?.toIso8601String(),
'isCompleted': t.isCompleted,
}).toList());
await prefs.setString('tasks', tasksJson);
}

5. Hình minh họa kiến trúc quản lý tasks

Task Manager Architecture

6. Best Practices

  • Sử dụng Provider hoặc Riverpod để quản lý state.
  • Tách biệt logic và UI.
  • Lưu trữ dữ liệu local hoặc cloud.
  • Sử dụng UUID cho id task.
  • Thêm xác nhận khi xóa task.

7. Tài liệu tham khảo

Flutter: Dự án thiết kế ứng dụng Todo List

· 8 min read

Todo List là một ứng dụng phổ biến và là dự án tuyệt vời để học Flutter. Bài viết này sẽ hướng dẫn bạn xây dựng một ứng dụng Todo List hoàn chỉnh với các tính năng cơ bản và nâng cao.

Todo List App

1. Cấu trúc dự án

1.1. Cấu trúc thư mục

lib/
├── models/
│ └── todo.dart
├── providers/
│ └── todo_provider.dart
├── screens/
│ ├── home_screen.dart
│ └── add_todo_screen.dart
├── widgets/
│ ├── todo_item.dart
│ └── todo_list.dart
└── main.dart

1.2. Cài đặt dependencies

dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
sqflite: ^2.0.0
path: ^1.8.0
intl: ^0.17.0

2. Model

2.1. Todo Model

class Todo {
final int? id;
final String title;
final String description;
final DateTime dueDate;
final bool isCompleted;
final Priority priority;

Todo({
this.id,
required this.title,
required this.description,
required this.dueDate,
this.isCompleted = false,
this.priority = Priority.medium,
});

Todo copyWith({
int? id,
String? title,
String? description,
DateTime? dueDate,
bool? isCompleted,
Priority? priority,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
dueDate: dueDate ?? this.dueDate,
isCompleted: isCompleted ?? this.isCompleted,
priority: priority ?? this.priority,
);
}

Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'description': description,
'dueDate': dueDate.toIso8601String(),
'isCompleted': isCompleted ? 1 : 0,
'priority': priority.index,
};
}

factory Todo.fromMap(Map<String, dynamic> map) {
return Todo(
id: map['id'],
title: map['title'],
description: map['description'],
dueDate: DateTime.parse(map['dueDate']),
isCompleted: map['isCompleted'] == 1,
priority: Priority.values[map['priority']],
);
}
}

enum Priority { low, medium, high }

3. Provider

3.1. Todo Provider

class TodoProvider extends ChangeNotifier {
List<Todo> _todos = [];
bool _isLoading = false;
String? _error;

List<Todo> get todos => _todos;
bool get isLoading => _isLoading;
String? get error => _error;

Future<void> loadTodos() async {
_isLoading = true;
_error = null;
notifyListeners();

try {
final db = await DatabaseHelper.instance.database;
final List<Map<String, dynamic>> maps = await db.query('todos');
_todos = maps.map((map) => Todo.fromMap(map)).toList();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> addTodo(Todo todo) async {
try {
final db = await DatabaseHelper.instance.database;
final id = await db.insert('todos', todo.toMap());
_todos.add(todo.copyWith(id: id));
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}

Future<void> updateTodo(Todo todo) async {
try {
final db = await DatabaseHelper.instance.database;
await db.update(
'todos',
todo.toMap(),
where: 'id = ?',
whereArgs: [todo.id],
);
final index = _todos.indexWhere((t) => t.id == todo.id);
_todos[index] = todo;
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}

Future<void> deleteTodo(int id) async {
try {
final db = await DatabaseHelper.instance.database;
await db.delete(
'todos',
where: 'id = ?',
whereArgs: [id],
);
_todos.removeWhere((todo) => todo.id == id);
notifyListeners();
} catch (e) {
_error = e.toString();
notifyListeners();
}
}
}

3.2. Database Helper

class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;

DatabaseHelper._init();

Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('todos.db');
return _database!;
}

Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);

return await openDatabase(
path,
version: 1,
onCreate: _createDB,
);
}

Future<void> _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE todos(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
dueDate TEXT NOT NULL,
isCompleted INTEGER NOT NULL,
priority INTEGER NOT NULL
)
''');
}
}

4. UI Components

4.1. Todo Item Widget

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);

@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => onToggle(),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(todo.description),
SizedBox(height: 4),
Text(
'Due: ${DateFormat('MMM dd, yyyy').format(todo.dueDate)}',
style: TextStyle(
color: todo.dueDate.isBefore(DateTime.now())
? Colors.red
: Colors.grey,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildPriorityIndicator(),
IconButton(
icon: Icon(Icons.delete),
onPressed: onDelete,
),
],
),
),
);
}

Widget _buildPriorityIndicator() {
Color color;
switch (todo.priority) {
case Priority.high:
color = Colors.red;
break;
case Priority.medium:
color = Colors.orange;
break;
case Priority.low:
color = Colors.green;
break;
}
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
);
}
}

4.2. Todo List Widget

class TodoList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<TodoProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Center(child: CircularProgressIndicator());
}

if (provider.error != null) {
return Center(
child: Text(
'Error: ${provider.error}',
style: TextStyle(color: Colors.red),
),
);
}

if (provider.todos.isEmpty) {
return Center(
child: Text('No todos yet. Add one!'),
);
}

return ListView.builder(
itemCount: provider.todos.length,
itemBuilder: (context, index) {
final todo = provider.todos[index];
return TodoItem(
todo: todo,
onToggle: () {
provider.updateTodo(
todo.copyWith(isCompleted: !todo.isCompleted),
);
},
onDelete: () {
provider.deleteTodo(todo.id!);
},
);
},
);
},
);
}
}

5. Screens

5.1. Home Screen

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
actions: [
IconButton(
icon: Icon(Icons.sort),
onPressed: () {
// Hiển thị dialog sắp xếp
},
),
],
),
body: TodoList(),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddTodoScreen()),
);
},
child: Icon(Icons.add),
),
);
}
}

5.2. Add Todo Screen

class AddTodoScreen extends StatefulWidget {
@override
_AddTodoScreenState createState() => _AddTodoScreenState();
}

class _AddTodoScreenState extends State<AddTodoScreen> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
DateTime _dueDate = DateTime.now();
Priority _priority = Priority.medium;

@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}

Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _dueDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(Duration(days: 365)),
);
if (picked != null && picked != _dueDate) {
setState(() {
_dueDate = picked;
});
}
}

void _submitForm() {
if (_formKey.currentState!.validate()) {
final todo = Todo(
title: _titleController.text,
description: _descriptionController.text,
dueDate: _dueDate,
priority: _priority,
);

context.read<TodoProvider>().addTodo(todo);
Navigator.pop(context);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Todo'),
),
body: Form(
key: _formKey,
child: ListView(
padding: EdgeInsets.all(16),
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
maxLines: 3,
),
SizedBox(height: 16),
ListTile(
title: Text('Due Date'),
subtitle: Text(
DateFormat('MMM dd, yyyy').format(_dueDate),
),
trailing: Icon(Icons.calendar_today),
onTap: _selectDate,
),
SizedBox(height: 16),
DropdownButtonFormField<Priority>(
value: _priority,
decoration: InputDecoration(
labelText: 'Priority',
border: OutlineInputBorder(),
),
items: Priority.values.map((priority) {
return DropdownMenuItem(
value: priority,
child: Text(priority.toString().split('.').last),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_priority = value;
});
}
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: Text('Add Todo'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
);
}
}

6. Main App

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TodoProvider(),
child: MaterialApp(
title: 'Todo List',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeScreen(),
),
);
}
}

7. Tính năng nâng cao

7.1. Sắp xếp và lọc

enum SortOption { dueDate, priority, title }

class TodoProvider extends ChangeNotifier {
// ... existing code ...

SortOption _sortOption = SortOption.dueDate;
bool _showCompleted = true;

List<Todo> get filteredTodos {
var todos = _todos;

if (!_showCompleted) {
todos = todos.where((todo) => !todo.isCompleted).toList();
}

switch (_sortOption) {
case SortOption.dueDate:
todos.sort((a, b) => a.dueDate.compareTo(b.dueDate));
break;
case SortOption.priority:
todos.sort((a, b) => b.priority.index.compareTo(a.priority.index));
break;
case SortOption.title:
todos.sort((a, b) => a.title.compareTo(b.title));
break;
}

return todos;
}

void setSortOption(SortOption option) {
_sortOption = option;
notifyListeners();
}

void toggleShowCompleted() {
_showCompleted = !_showCompleted;
notifyListeners();
}
}

7.2. Thông báo

class NotificationService {
static Future<void> scheduleNotification(Todo todo) async {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();

const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');

final InitializationSettings initializationSettings =
InitializationSettings(android: initializationSettingsAndroid);

await flutterLocalNotificationsPlugin.initialize(initializationSettings);

await flutterLocalNotificationsPlugin.zonedSchedule(
todo.id!,
'Todo Reminder',
todo.title,
TZDateTime.from(todo.dueDate, local),
NotificationDetails(
android: AndroidNotificationDetails(
'todo_reminders',
'Todo Reminders',
importance: Importance.high,
priority: Priority.high,
),
),
androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
}

Kết luận

Ứng dụng Todo List là một dự án tuyệt vời để học Flutter. Nó bao gồm nhiều khái niệm quan trọng như:

  • Quản lý state với Provider
  • Lưu trữ dữ liệu local với SQLite
  • Xây dựng UI phức tạp
  • Xử lý form và validation
  • Quản lý thông báo

Bạn có thể mở rộng dự án này bằng cách thêm các tính năng như:

  • Đồng bộ hóa với backend
  • Phân loại todo theo danh mục
  • Tìm kiếm và lọc nâng cao
  • Giao diện người dùng tùy chỉnh
  • Hỗ trợ đa ngôn ngữ

Tài liệu tham khảo: