Chủ đích: Hiểu vị trí đặt state trong widget tree để chia sẻ dữ liệu hiệu quả
- Phân tích cây widget và xác định ancestor chung (common ancestor)
- Hiểu tại sao phải đặt state ở ancestor chung
- Thiết lập cấu trúc dự án cho state management
MaterialApp
└── HomeScreen
├── StudentList
│ └── StudentCard (cần đọc students)
└── OverviewCard (cần đọc average)
ScoreEntryScreen (cần gọi setScore)
Vấn đề:
StudentListcần danh sách sinh viênOverviewCardcần điểm trung bìnhScoreEntryScreencần hàm cập nhật điểm- Nếu dùng props: phải truyền qua nhiều lớp → Prop Drilling Hell!
MaterialApp
└── GradeProvider ← ĐẶT STATE Ở ĐÂY (ancestor chung)
├── HomeScreen
│ ├── StudentList ← Truy cập trực tiếp
│ └── OverviewCard ← Truy cập trực tiếp
└── ScoreEntryScreen ← Truy cập trực tiếp
- Ancestor chung của tất cả widgets cần data
- Bọc MaterialApp → toàn bộ app có thể truy cập
- Không cần props → widgets con tự lấy dữ liệu
| Vị trí | Ưu điểm | Nhược điểm | Kết luận |
|---|---|---|---|
| Trong HomeScreen | Gần UI | ScoreEntryScreen không truy cập được | ❌ Không đủ cao |
| Trong MaterialApp | Phạm vi toàn app | Khó tách logic state | |
| GradeProvider wrap MaterialApp | Phạm vi toàn app + tách logic | Cần thêm 1 layer | ✅ Tối ưu nhất |
lib/
├── main.dart # Entry point, wrap GradeProvider
├── models/
│ └── student.dart # Data model
├── providers/
│ └── grade_provider.dart # ← VỊ TRÍ CHỨA STATE
├── screens/
│ ├── home_screen.dart
│ └── score_entry_screen.dart
└── widgets/
├── student_list.dart # Consumer widget
└── overview.dart # Consumer widget
// lib/models/student.dart
class Student {
final String id;
final String name;
double? score; // Nullable - có thể chưa có điểm
Student({
required this.id,
required this.name,
this.score,
});
}┌─────────────────────────────────────┐
│ GradeProvider (State Here) │
│ • List<Student> students │
│ • double average │
│ • setScore(id, score) │
└──────────────┬──────────────────────┘
│
┌────────┼────────┐
│ │ │
┌───▼───┐ ┌──▼──┐ ┌──▼─────────────┐
│ List │ │ Card│ │ ScoreEntry │
│ Widget│ │Widget│ │ Screen │
└───────┘ └─────┘ └────────────────┘
(đọc) (đọc) (đọc + ghi)
Q1: Tại sao không đặt state trong HomeScreen? → Vì ScoreEntryScreen (màn hình khác) cũng cần truy cập state
Q2: Nếu có thêm màn hình Statistics, đặt Provider ở đâu vẫn hoạt động? → Vẫn ở vị trí hiện tại (wrap MaterialApp), vì nó là ancestor của tất cả routes
Q3: Prop drilling là gì? → Truyền props qua nhiều lớp widget trung gian (không dùng props đó) chỉ để đến widget con
- Hiểu khái niệm "ancestor chung" trong widget tree
- Vẽ được sơ đồ cây widget của app
- Xác định đúng vị trí đặt GradeProvider (wrap MaterialApp)
- Tạo đủ cấu trúc thư mục: models, providers, screens, widgets
- File
student.dartchạy không lỗi
Chủ đích: Tạo lớp Provider kết hợp StatefulWidget + InheritedWidget để quản lý state
- Hiểu kiến trúc 2 lớp: StatefulWidget (chứa state) + InheritedWidget (chia sẻ state)
- Implement GradeProvider với data và methods
- Hiểu cơ chế
updateShouldNotify()để tối ưu rebuild
┌──────────────────────────────────────┐
│ GradeProvider (StatefulWidget) │
│ • Quản lý state (students, version)│
│ • Logic nghiệp vụ (setScore, avg) │
└──────────────┬───────────────────────┘
│ wraps
┌──────────────▼───────────────────────┐
│ GradeInherited (InheritedWidget) │
│ • Chia sẻ data xuống cây widget │
│ • Quyết định khi nào rebuild │
└──────────────────────────────────────┘
// lib/providers/grade_provider.dart
import 'package:flutter/material.dart';
import '../models/student.dart';
class GradeProvider extends StatefulWidget {
final Widget child; // Widget con (thường là MaterialApp)
const GradeProvider({super.key, required this.child});
@override
State<GradeProvider> createState() => _GradeProviderState();
// 🔑 Helper method: Truy cập Provider từ bất kỳ đâu
static GradeInherited of(BuildContext context) {
return GradeInherited._of(context);
}
}Giải thích:
child: Widget con mà Provider sẽ wrap (MaterialApp)of(context): Static method để widgets con dễ dàng truy cập- Stateful vì cần
setState()để cập nhật UI
class _GradeProviderState extends State<GradeProvider> {
// 📊 STATE: Danh sách sinh viên
List<Student> students = [
Student(id: 's1', name: 'Nguyễn Văn A'),
Student(id: 's2', name: 'Trần Thị B'),
Student(id: 's3', name: 'Lê Văn C'),
Student(id: 's4', name: 'Phạm Thị D'),
];
// 🔢 VERSION: Đánh dấu mỗi lần thay đổi
int _version = 0;
// 🔄 METHOD: Cập nhật điểm sinh viên
void setScore(String id, double? score) {
setState(() {
final s = students.firstWhere((e) => e.id == id);
s.score = score;
_version++; // Tăng version → trigger rebuild
});
}
// 📈 COMPUTED: Tính điểm trung bình
double get average {
final scores = students
.map((s) => s.score)
.where((s) => s != null)
.cast<double>()
.toList();
if (scores.isEmpty) return 0.0;
return scores.reduce((a, b) => a + b) / scores.length;
}
@override
Widget build(BuildContext context) {
// Wrap child bằng InheritedWidget
return GradeInherited(
students: students,
setScore: setScore,
average: average,
version: _version,
child: widget.child, // MaterialApp
);
}
}Giải thích:
students,setScore,average,version: Dữ liệu được chia sẻ- Wrap
widget.childđể truyền data xuống cây widget - Mỗi lần
setState()→_version++→ build lạiGradeInherited
class GradeInherited extends InheritedWidget {
// 📦 DATA: Dữ liệu được chia sẻ
final List<Student> students;
final void Function(String id, double? score) setScore;
final double average;
final int version;
const GradeInherited({
super.key,
required this.students,
required this.setScore,
required this.average,
required this.version,
required super.child,
});
// 🔍 HELPER: Tìm InheritedWidget trong context tree
static GradeInherited _of(BuildContext context) {
final result = context
.dependOnInheritedWidgetOfExactType<GradeInherited>();
assert(result != null, 'No GradeInherited found in context');
return result!;
}
// 🔔 NOTIFICATION: Quyết định khi nào rebuild widgets con
@override
bool updateShouldNotify(covariant GradeInherited oldWidget) {
return version != oldWidget.version;
}
}Giải thích từng phần:
// Flutter tìm InheritedWidget gần nhất có type GradeInherited
// và TỰ ĐỘNG đăng ký phụ thuộc (dependency)
context.dependOnInheritedWidgetOfExactType<GradeInherited>()// So sánh version mới vs cũ
// return true → Flutter rebuild các widgets đã đăng ký phụ thuộc
// return false → Không rebuild (tối ưu performance)
return version != oldWidget.version;| Cách | Code | Performance | Khi nào dùng |
|---|---|---|---|
| Deep equality | students != oldWidget.students |
❌ Chậm (so sánh từng phần tử) | List nhỏ (<10 items) |
| Version tracking | version != oldWidget.version |
✅ Nhanh (so sánh 1 số) | List lớn, update thường xuyên |
User nhập điểm (8.5)
↓
setScore('s1', 8.5) được gọi
↓
setState(() {
students[0].score = 8.5
_version++ (0 → 1)
})
↓
build() được gọi → tạo GradeInherited mới
↓
updateShouldNotify(old.version=0, new.version=1) → return true
↓
Flutter notify tất cả widgets đã gọi dependOnInheritedWidget
↓
StudentList.build() + OverviewCard.build() được gọi lại
↓
UI cập nhật: Avatar đổi màu, Average = 8.5
Q1: Tại sao cần StatefulWidget + InheritedWidget (2 lớp)?
→ StatefulWidget giữ state có thể thay đổi (setState), InheritedWidget chia sẻ state xuống cây widget
Q2: Nếu không tăng _version, điều gì xảy ra?
→ updateShouldNotify() return false → widgets con không rebuild → UI không cập nhật
Q3: dependOnInheritedWidgetOfExactType làm gì?
→ Tìm InheritedWidget trong ancestor tree VÀ đăng ký phụ thuộc tự động
- Hiểu vai trò của StatefulWidget (chứa state)
- Hiểu vai trò của InheritedWidget (chia sẻ state)
- Code
grade_provider.dartchạy không lỗi - Hiểu
updateShouldNotify()hoạt động thế nào - Hiểu tại sao dùng
versionthay vì so sánh deep equality
User action (nhập điểm)
↓
setScore() được gọi
↓
setState() + _version++
↓
GradeInherited rebuild với version mới
↓
updateShouldNotify() return true
↓
Các widget con được notify rebuild
- GradeProvider chạy không lỗi
- Hiểu vai trò của
version - Hiểu
updateShouldNotify()hoạt động thế nào
Chủ đích: Sử dụng Provider.of(context) để đọc dữ liệu và tự động rebuild
- Hiểu cách gọi
GradeProvider.of(context)để lấy data - Xây dựng UI widgets tiêu thụ (consume) dữ liệu từ Provider
- Hiểu cơ chế tự động rebuild khi Provider thay đổi
// Trong bất kỳ widget con nào
@override
Widget build(BuildContext context) {
// Gọi static method GradeProvider.of()
final provider = GradeProvider.of(context);
// provider bây giờ chứa GradeInherited với data
final students = provider.students;
final average = provider.average;
final setScore = provider.setScore;
// Sử dụng data để build UI...
}// Bên trong GradeProvider.of()
static GradeInherited of(BuildContext context) {
return GradeInherited._of(context);
}
// Bên trong GradeInherited._of()
static GradeInherited _of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<GradeInherited>();
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// KEY: Đăng ký phụ thuộc tự động!
return result!;
}Điều gì xảy ra:
- Flutter tìm
GradeInheritedgần nhất trong ancestor tree - Đăng ký widget hiện tại vào danh sách phụ thuộc của
GradeInherited - Khi
GradeInheritedrebuild +updateShouldNotify()return true → widget này tự động rebuild
// lib/widgets/student_list.dart
import 'package:flutter/material.dart';
import '../providers/grade_provider.dart';
import '../screens/score_entry_screen.dart';
class StudentList extends StatelessWidget {
const StudentList({super.key});
Color _scoreColor(double? score) {
if (score == null) return Colors.grey.shade400;
if (score >= 8) return Colors.green;
if (score >= 5) return Colors.orange;
return Colors.red;
}
String _initials(String name) {
final parts = name.split(' ');
if (parts.length == 1) return parts.first[0].toUpperCase();
return '${parts[0][0]}${parts.last[0]}'.toUpperCase();
}
@override
Widget build(BuildContext context) {
// 🔑 TRUY XUẤT DỮ LIỆU TỪ PROVIDER
final provider = GradeProvider.of(context);
final students = provider.students;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
itemCount: students.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final s = students[index];
final bg = _scoreColor(s.score);
return Card(
elevation: 2,
child: ListTile(
leading: CircleAvatar(
backgroundColor: bg,
child: Text(_initials(s.name))
),
title: Text(
s.name,
style: const TextStyle(fontWeight: FontWeight.w600)
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Điểm: ${s.score != null ? s.score!.toStringAsFixed(1) : "Chưa có"}'),
if (s.score != null)
LinearProgressIndicator(
value: (s.score! / 10).clamp(0.0, 1.0),
color: bg
),
],
),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ScoreEntryScreen(studentId: s.id)
)
),
),
);
},
);
}
}Phân tích:
GradeProvider.of(context)→ Đăng ký phụ thuộc- Lấy
studentsđể render ListView - Mỗi khi điểm thay đổi →
_version++→ StudentList tự động rebuild
// lib/widgets/overview.dart
import 'package:flutter/material.dart';
import '../providers/grade_provider.dart';
class OverviewCard extends StatelessWidget {
const OverviewCard({super.key});
@override
Widget build(BuildContext context) {
// 🔑 TRUY XUẤT DỮ LIỆU TỪ PROVIDER
final provider = GradeProvider.of(context);
final avg = provider.average;
final count = provider.students.length;
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Box điểm trung bình
Container(
width: 84,
height: 84,
decoration: BoxDecoration(
color: Colors.green[50],
borderRadius: BorderRadius.circular(8)
),
child: Center(
child: Text(
avg.toStringAsFixed(2),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold
)
)
),
),
const SizedBox(width: 16),
// Thông tin chi tiết
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tổng quan lớp',
style: Theme.of(context).textTheme.titleMedium
),
const SizedBox(height: 8),
Text('Số sinh viên: $count'),
const SizedBox(height: 12),
LinearProgressIndicator(
value: (avg / 10).clamp(0.0, 1.0)
),
],
),
),
],
),
),
);
}
}Phân tích:
GradeProvider.of(context)→ Đăng ký phụ thuộc- Lấy
average(computed property) - Mỗi khi
setScore()được gọi → average tự động tính lại → OverviewCard tự động rebuild
┌─────────────────────────────────┐
│ GradeInherited (version=1) │
│ students: [{id:'s1',score:8}] │
│ average: 8.0 │
└────────────┬────────────────────┘
│
┌────────┴────────┐
│ │
┌───▼──────────┐ ┌──▼─────────────┐
│ StudentList │ │ OverviewCard │
│ .of(context) │ │ .of(context) │
│ → đăng ký │ │ → đăng ký │
└──────────────┘ └────────────────┘
[User nhập điểm mới: 9.0]
↓
setScore('s1', 9.0)
↓
setState() → _version++ (1→2)
↓
┌─────────────────────────────────┐
│ GradeInherited (version=2) │ ← Rebuild
│ students: [{id:'s1',score:9}] │
│ average: 9.0 │
└────────────┬────────────────────┘
│
┌────────┴────────┐
│ │
┌───▼──────────┐ ┌──▼─────────────┐
│ StudentList │ │ OverviewCard │
│ rebuild! │ │ rebuild! │
│ (score=9.0) │ │ (avg=9.0) │
└──────────────┘ └────────────────┘
Q1: Tại sao gọi of(context) TRONG build(), không phải NGOÀI?
→ Vì dependOnInheritedWidget chỉ hoạt động trong build context, và cần đăng ký lại mỗi lần rebuild
Q2: Widget nào sẽ rebuild khi setScore() được gọi?
→ Tất cả widgets đã gọi GradeProvider.of(context) trong build method (StudentList, OverviewCard)
Q3: Nếu gọi of(context) trong initState(), có tự động rebuild không?
→ KHÔNG! Phải gọi trong build() hoặc dùng didChangeDependencies()
- StudentList hiển thị đúng màu Avatar theo điểm
- OverviewCard hiển thị điểm trung bình
- Hiểu tại sao phải gọi
of(context)trong build() - Hiểu widgets nào sẽ rebuild khi state thay đổi
Chủ đích: Gọi methods từ Provider để thay đổi state và trigger UI update
- Xây dựng màn hình nhập điểm với UX tốt
- Gọi
setScore()để cập nhật state - Kiểm tra luồng dữ liệu end-to-end: Input → State → UI Update
┌────────────────────┐
│ Widget con │
│ (ScoreEntryScreen)│
└────────┬───────────┘
│
│ 1. Lấy method
│ provider.setScore
↓
┌────────────────────┐
│ GradeProvider │
│ setScore(id, val) │
└────────┬───────────┘
│
│ 2. setState()
│ + _version++
↓
┌────────────────────┐
│ GradeInherited │
│ rebuild với │
│ version mới │
└────────┬───────────┘
│
│ 3. Notify widgets
│ đã đăng ký
↓
┌────────────────────┐
│ StudentList + │
│ OverviewCard │
│ rebuild UI │
└────────────────────┘
// lib/screens/score_entry_screen.dart
import 'package:flutter/material.dart';
import '../providers/grade_provider.dart';
class ScoreEntryScreen extends StatefulWidget {
final String studentId;
const ScoreEntryScreen({super.key, required this.studentId});
@override
State<ScoreEntryScreen> createState() => _ScoreEntryScreenState();
}
class _ScoreEntryScreenState extends State<ScoreEntryScreen> {
final _controller = TextEditingController();
double? _sliderValue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Đọc điểm hiện tại (chỉ 1 lần khi màn hình load)
final provider = GradeProvider.of(context);
final student = provider.students.firstWhere((s) => s.id == widget.studentId);
_controller.text = student.score?.toString() ?? '';
_sliderValue = student.score;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _save() {
double? value;
// Parse value từ TextField hoặc Slider
if (_controller.text.trim().isNotEmpty) {
value = double.tryParse(_controller.text.replaceAll(',', '.'));
}
if (_sliderValue != null) value = _sliderValue;
// 🔑 KEY POINT: GỌI METHOD TỪ PROVIDER ĐỂ THAY ĐỔI STATE
GradeProvider.of(context).setScore(widget.studentId, value);
// Quay về màn hình trước → StudentList sẽ tự động cập nhật
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
final provider = GradeProvider.of(context);
final student = provider.students.firstWhere((s) => s.id == widget.studentId);
return Scaffold(
appBar: AppBar(title: Text('Nhập điểm: ${student.name}')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Slider input
Slider(
value: (_sliderValue ?? 0.0).clamp(0.0, 10.0),
min: 0,
max: 10,
divisions: 20,
label: _sliderValue?.toStringAsFixed(1) ?? 'Chưa chọn',
onChanged: (v) => setState(() {
_sliderValue = v;
_controller.text = v.toStringAsFixed(1);
}),
),
// TextField input
TextField(
controller: _controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Điểm (0-10)'
)
),
const SizedBox(height: 16),
// Action buttons
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _save,
child: const Text('Lưu')
)
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: () => setState(() {
_controller.clear();
_sliderValue = null;
}),
child: const Text('Xóa')
),
],
),
],
),
),
);
}
}Giải thích chi tiết:
// Gọi 1 lần khi màn hình load
void didChangeDependencies() {
super.didChangeDependencies();
final provider = GradeProvider.of(context);
// Load điểm cũ vào form
_sliderValue = provider.students.firstWhere(...).score;
}// Local state: Slider + TextField
double? _sliderValue;
TextEditingController _controller;
// Sync 2 chiều
Slider(
value: _sliderValue,
onChanged: (v) => setState(() {
_sliderValue = v;
_controller.text = v.toString(); // Sync
})
)void _save() {
double? value = _sliderValue; // Lấy từ local state
// 🔥 THAY ĐỔI TRẠNG THÁI TOÀN CỤC
GradeProvider.of(context).setScore(widget.studentId, value);
// ↑↑↑↑↑↑↑↑
// Trigger rebuild toàn bộ app
Navigator.pop(context); // Quay về
}// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import '../widgets/student_list.dart';
import '../widgets/overview.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Danh sách sinh viên')),
body: Column(
children: const [
Expanded(child: StudentList()), // Đọc students
SizedBox(height: 12),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: OverviewCard() // Đọc average
),
SizedBox(height: 12),
],
),
);
}
}// lib/main.dart
import 'package:flutter/material.dart';
import 'providers/grade_provider.dart';
import 'screens/home_screen.dart';
void main() => runApp(const GradeApp());
class GradeApp extends StatelessWidget {
const GradeApp({super.key});
@override
Widget build(BuildContext context) {
return GradeProvider( // 🔑 ĐẶT PROVIDER Ở VỊ TRÍ CAO NHẤT
child: MaterialApp(
title: 'Ứng dụng quản lý điểm sinh viên',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomeScreen(),
debugShowCheckedModeBanner: false,
),
);
}
}[1] User nhập điểm 8.5 trên ScoreEntryScreen
↓
[2] Bấm nút "Lưu" → _save() được gọi
↓
[3] GradeProvider.of(context).setScore('s1', 8.5)
↓
[4] Bên trong GradeProvider:
setState(() {
students[0].score = 8.5;
_version++; // 0 → 1
})
↓
[5] Flutter gọi build() → tạo GradeInherited mới
GradeInherited(
version: 1, // Mới
students: [...], // Đã update
average: 8.5, // Tính lại
)
↓
[6] updateShouldNotify(old, new)
→ old.version(0) != new.version(1)
→ return true
↓
[7] Flutter notify các widgets đã đăng ký:
- StudentList.build() được gọi lại
- OverviewCard.build() được gọi lại
↓
[8] UI cập nhật:
✓ Avatar đổi màu xanh
✓ Progress bar hiển thị
✓ OverviewCard hiển thị average = 8.5
✓ Thấy 4 sinh viên chưa có điểm
✓ OverviewCard hiển thị average = 0.0
1. Tap "Nguyễn Văn A"
2. Kéo Slider → 8.5
3. TextField tự động hiển thị "8.5"
4. Bấm "Lưu"
✓ Avatar "Nguyễn Văn A" → Màu xanh (≥8)
✓ Progress bar xuất hiện (85%)
✓ OverviewCard: average = 8.5
1. Tap "Trần Thị B"
2. Nhập TextField: "6.0"
3. Bấm "Lưu"
✓ Avatar "Trần Thị B" → Màu cam (5-8)
✓ OverviewCard: average = (8.5 + 6.0) / 2 = 7.25
Q1: Widget nào được rebuild khi gọi setScore()?
→ StudentList và OverviewCard (vì cả hai gọi GradeProvider.of(context) trong build)
Q2: Tại sao Navigator.pop() vẫn thấy UI cập nhật ngay?
→ Vì setState() đã được gọi TRƯỚC pop(), StudentList đã đăng ký rebuild
Q3: Nếu không gọi setState() trong setScore, điều gì xảy ra? → Data thay đổi NHƯNG UI không update (vì Flutter không biết cần rebuild)
| Khái niệm | Giải thích | Code |
|---|---|---|
| 1. Vị trí state | Đặt ở ancestor chung của tất cả widgets cần data | GradeProvider wrap MaterialApp |
| 2. Chia sẻ data | InheritedWidget + of(context) |
GradeProvider.of(context) trong build() |
| 3. Thay đổi state | setState() + version tracking |
setState(() { _version++; }) |
| Lỗi | Nguyên nhân | Giải pháp |
|---|---|---|
GradeProvider.of(context) null |
Provider không đúng scope | Kiểm tra GradeProvider wrap MaterialApp |
| Widget không rebuild | Không gọi of(context) trong build |
Gọi trong build(), KHÔNG trong initState() |
| Hot reload không update | Stateful widget cache | Hot restart (Ctrl+Shift+F5) |
- Level 1: Thêm nút "Xóa điểm" trong ScoreEntryScreen
- Level 2: Thêm màn hình thống kê (số sinh viên giỏi/TB/yếu)
- Level 3: Refactor sang
ChangeNotifier+providerpackage
- App chạy mượt, không lỗi
- Nhập điểm cho ít nhất 3 sinh viên
- Average tự động cập nhật đúng
- Hiểu 4 phần: Vị trí state → Xây Provider → Truy xuất data → Thay đổi state
- Có thể vẽ sơ đồ luồng dữ liệu
- Flutter InheritedWidget Docs
- Provider Package
- State Management Overview
- Source code hoàn chỉnh: thư mục
lib/