📦 Provider
1. Giới thiệu
Phần tiêu đề “1. Giới thiệu”Provider là giải pháp state management đơn giản và được Flutter team khuyến nghị cho hầu hết ứng dụng.
dependencies: provider: ^6.1.0Luồng dữ liệu (Data Flow)
Phần tiêu đề “Luồng dữ liệu (Data Flow)”Provider giúp luồng dữ liệu trở nên rõ ràng và một chiều (unidirectional):
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ ChangeNotifier │ notify() │ Provider │ rebuild() │ Widget ││ (Model) │ ──────────▶ │ (Root) │ ──────────▶ │ (UI) ││ │ │ │ │ ││ _count = 0 │ │ │ │ Text("0") │└────────┬────────┘ └─────────────────┘ └────────┬────────┘ ▲ │ │ │ └───────────────────────────────────────────────────────────────┘ user interaction (onTap)2. Cài đặt cơ bản
Phần tiêu đề “2. Cài đặt cơ bản”Tạo Model với ChangeNotifier
Phần tiêu đề “Tạo Model với ChangeNotifier”import 'package:flutter/foundation.dart';
class Counter extends ChangeNotifier { int _count = 0;
int get count => _count;
void increment() { _count++; notifyListeners(); // Thông báo cho listeners }
void decrement() { _count--; notifyListeners(); }}Provide ở root
Phần tiêu đề “Provide ở root”void main() { runApp( ChangeNotifierProvider( create: (context) => Counter(), child: MyApp(), ), );}Consume trong widget
Phần tiêu đề “Consume trong widget”class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // Đọc giá trị Text('Count: ${context.watch<Counter>().count}'),
// Gọi method ElevatedButton( onPressed: () => context.read<Counter>().increment(), child: Text('Increment'), ), ], ); }}3. context.watch vs context.read
Phần tiêu đề “3. context.watch vs context.read”| Method | Mô tả | Khi nào dùng |
|---|---|---|
context.watch<T>() |
Lắng nghe thay đổi, rebuild widget | Trong build() |
context.read<T>() |
Chỉ đọc, không rebuild | Trong callbacks |
@overrideWidget build(BuildContext context) { // watch: rebuild khi counter thay đổi final count = context.watch<Counter>().count;
return Column( children: [ Text('$count'), ElevatedButton( // read: chỉ gọi method, không cần rebuild onPressed: () => context.read<Counter>().increment(), child: Text('+'), ), ], );}4. Consumer Widget
Phần tiêu đề “4. Consumer Widget”Alternative cho context.watch:
Consumer<Counter>( builder: (context, counter, child) { return Text('Count: ${counter.count}'); },)Với child optimization:
Consumer<Counter>( builder: (context, counter, child) { return Row( children: [ Text('${counter.count}'), child!, // Không rebuild ], ); }, child: const Icon(Icons.star), // const, không rebuild)5. Selector - Rebuild tối ưu
Phần tiêu đề “5. Selector - Rebuild tối ưu”Chỉ rebuild khi giá trị cụ thể thay đổi:
Selector<User, String>( selector: (context, user) => user.name, // Chỉ select name builder: (context, name, child) { return Text(name); // Chỉ rebuild khi name thay đổi },)6. Multiple Providers
Phần tiêu đề “6. Multiple Providers”void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => Counter()), ChangeNotifierProvider(create: (_) => ThemeProvider()), Provider(create: (_) => ApiService()), ], child: MyApp(), ), );}7. Ví dụ thực tế - Todo App
Phần tiêu đề “7. Ví dụ thực tế - Todo App”Model
Phần tiêu đề “Model”class Todo { final String id; final String title; bool isCompleted;
Todo({required this.id, required this.title, this.isCompleted = false});}
class TodoProvider extends ChangeNotifier { final List<Todo> _todos = [];
List<Todo> get todos => _todos; List<Todo> get completedTodos => _todos.where((t) => t.isCompleted).toList(); int get remainingCount => _todos.where((t) => !t.isCompleted).length;
void add(String title) { _todos.add(Todo(id: DateTime.now().toString(), title: title)); notifyListeners(); }
void toggle(String id) { final todo = _todos.firstWhere((t) => t.id == id); todo.isCompleted = !todo.isCompleted; notifyListeners(); }
void remove(String id) { _todos.removeWhere((t) => t.id == id); notifyListeners(); }}class TodoPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Todos (${context.watch<TodoProvider>().remainingCount} left)'), ), body: Consumer<TodoProvider>( builder: (context, provider, child) { return ListView.builder( itemCount: provider.todos.length, itemBuilder: (context, index) { final todo = provider.todos[index]; return ListTile( leading: Checkbox( value: todo.isCompleted, onChanged: (_) => provider.toggle(todo.id), ), title: Text(todo.title), trailing: IconButton( icon: Icon(Icons.delete), onPressed: () => provider.remove(todo.id), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddDialog(context), child: Icon(Icons.add), ), ); }}8. Best Practices
Phần tiêu đề “8. Best Practices”- Tách logic ra Provider - Widget chỉ hiển thị UI
- Dùng
context.readcho callbacks - Tránh rebuild không cần thiết - Dùng Selector - Khi chỉ cần một phần của state
- Dispose resources - Override dispose() nếu cần
📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Component | Vai trò |
|---|---|
ChangeNotifier |
Base class cho state |
ChangeNotifierProvider |
Provide state cho widget tree |
context.watch |
Lắng nghe và rebuild |
context.read |
Đọc một lần |
Consumer |
Widget alternative cho watch |
Selector |
Tối ưu rebuild |