Phiên bản: 2.0 (Cập nhật theo dự án thực tế)
Tác giả: HuyHoangDevVN
Ngày: 2025-11-04
Mục đích: Hướng dẫn xây dựng ứng dụng quản lý điểm cho nhiều sinh viên với:
- Danh sách sinh viên + điểm (màn hình chính)
- Màn hình nhập/cập nhật điểm cho từng sinh viên
- Thẻ tổng quan hiển thị điểm trung bình lớp
- Khi cập nhật điểm bất kỳ sinh viên nào → điểm trung bình tự động cập nhật ở mọi nơi
→ Đây là ứng dụng thực tế của InheritedWidget: chia sẻ state xuống cây widget, tự động rebuild các widget phụ thuộc.
- Nhắc lại bài cũ (Widget tree & State management)
- Giới thiệu dự án (Cấu trúc & yêu cầu)
- Mục tiêu (theo thang Bloom ≥ 3)
- Thực hành — CHI TIẾT (8 bước)
- Cấu trúc thư mục
- Model Student
- GradeProvider (InheritedWidget)
- Screens & Widgets
- Navigation & State update
- Tổng kết & lượng hóa
- Phiếu phát tay / Cheat-sheet
- Tài nguyên & bước tiếp theo
- NHẮC LẠI BÀI CŨ (Widget tree & State) — (0:00–0:05)
- Mục đích: Hướng dẫn xây dựng ứng dụng quản lý điểm cho nhiều sinh viên với:
- Danh sách sinh viên + điểm (màn hình chính với Card, Avatar, Progress bar)
- Màn hình nhập/cập nhật điểm cho từng sinh viên (với Slider và TextField)
- Thẻ tổng quan hiển thị điểm trung bình lớp
- Khi cập nhật điểm bất kỳ sinh viên nào → điểm trung bình tự động cập nhật ở mọi nơi
→ Đây là ứng dụng thực tế của InheritedWidget: chia sẻ state xuống cây widget, tự động rebuild các widget phụ thuộc.
- Lời dẫn mẫu (đọc nguyên xi):
“Widget tree là cấu trúc giao diện ứng dụng biểu diễn quan hệ cha con giữa các widget, bắt đầu từ widget gốc chứa toàn bộ màn hình. State là dữ liệu có thể thay đổi (ví dụ: điểm sinh viên). Nếu nhiều widget cùng cần một giá trị, ta cần một ‘nguồn duy nhất’. Hôm nay ta sẽ học cách tạo nguồn đó bằng InheritedWidget.”
- Hoạt động: Hiển thị sơ đồ StudentScorePage → StudentNameText, ScoreDisplay, ScoreActionButtons; hỏi học viên “Widget nào cần score?” và “Score nên đặt ở đâu?”
- Kết quả: Học viên nắm lại khái niệm widget tree và lift state up.
- NỘI DUNG (Liệt kê 4 mục thực hành) — (0:05–0:07)
- Liệt kê để học viên thấy tổng quan:
- Xác định vị trí lưu state trong widget tree.
- Viết
ScoreProviderkế thừaInheritedWidget. - Lấy dữ liệu từ Provider ở widget con (
ScoreDisplay,ScoreActionButtons). - Thay đổi state ở
ScoreManagervà quan sát UI cập nhật.
- MỤC TIÊU (Bloom ≥ 3) — (0:07–0:09)
- Mục tiêu theo thang Bloom:
- Vận dụng (Apply): Viết và tích hợp
ScoreProviderđể chia sẻ điểm. - Phân tích (Analyze): Phân tích widget tree và chỉ rõ nơi đặt state.
- Đánh giá (Evaluate): So sánh hành vi rebuild giữa widget lấy trong
build()và widget “capture once”. - Ứng dụng nâng cao: Biết khi nào nên dùng InheritedWidget trực tiếp và khi nào nên chuyển sang
provider/ChangeNotifier.
- Vận dụng (Apply): Viết và tích hợp
- Tiêu chí đánh giá (lượng hóa):
- Thực hành (60%): Project chạy, nút + / - hoạt động,
ScoreDisplayindebugPrintkhi thay đổi. - Phân tích (30%): Học viên nêu được nơi đặt state và lý do (1 câu).
- Đánh giá (10%): Học viên giải thích khác biệt capture vs dependent.
- Thực hành (60%): Project chạy, nút + / - hoạt động,
Phần 4 được chia thành 8 bước nhỏ. Mỗi bước có: mục tiêu, thời gian gợi ý, lời dẫn mẫu, hoạt động học viên, kết quả mong đợi, kiểm tra nhanh.
Mục tiêu: Học viên hiểu cấu trúc dự án, tổ chức code theo module.
Lời dẫn mẫu:
"Ta sẽ tổ chức code theo cấu trúc rõ ràng: models cho dữ liệu, providers cho state management, screens cho màn hình, widgets cho các thành phần tái sử dụng."
Cấu trúc:
lib/
├── main.dart
├── models/
│ └── student.dart
├── providers/
│ └── grade_provider.dart
├── screens/
│ ├── home_screen.dart
│ └── score_entry_screen.dart
└── widgets/
├── student_list.dart
└── overview.dart
Hoạt động: Học viên tạo các thư mục trong project.
Mục tiêu: Tạo class Student lưu id, name, score.
Code mẫu (lib/models/student.dart):
class Student {
final String id;
final String name;
double? score;
Student({required this.id, required this.name, this.score});
}Giải thích:
id: unique identifiername: tên sinh viênscore: nullable (có thể chưa có điểm)
Kiểm tra: "Tại sao score là nullable?" → Vì sinh viên có thể chưa có điểm.
- Mục tiêu: Hiểu cấu trúc ScoreProvider, static
of(context),updateShouldNotify. - Lời dẫn mẫu:
“Ta làm một ‘hộp điểm’ gọi là ScoreProvider — chỉ truyền dữ liệu và callback; không tự thay đổi giá trị.”
- Code mẫu copy‑paste (sử dụng dưới main.dart):
class ScoreProvider extends InheritedWidget {
final int score;
final VoidCallback? onIncrement;
final VoidCallback? onDecrement;
const ScoreProvider({
Key? key,
required this.score,
this.onIncrement,
this.onDecrement,
required Widget child,
}) : super(key: key, child: child);
static ScoreProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ScoreProvider>();
}
@override
bool updateShouldNotify(covariant ScoreProvider oldWidget) {
return oldWidget.score != score;
}
}-
Giải thích từng dòng bằng câu ngắn:
score: dữ liệu chia sẻ.onIncrement/onDecrement: callback do ScoreManager cung cấp.of(context): cách widget con lấy provider.updateShouldNotify: so sánh cũ/mới để decide rebuild.
-
Học viên: copy/paste vào project, đọc comment.
-
Kiểm tra nhanh: hỏi “of(context) dùng API nào?” →
dependOnInheritedWidgetOfExactType. -
Xử lý lỗi: kiểm tra import
material.dart.4.3 Tạo
ScoreManager(Stateful) (0:23–0:30) — 7 phút -
Mục tiêu: Lưu và thay đổi
score. -
Lời dẫn mẫu:
“ScoreManager chứa
_score. Khi gọisetState, ScoreManager tạo provider mới; các con đăng ký sẽ được rebuild.” -
Code mẫu:
class ScoreManager extends StatefulWidget {
final Widget child;
const ScoreManager({Key? key, required this.child}) : super(key: key);
@override
State<ScoreManager> createState() => _ScoreManagerState();
}
class _ScoreManagerState extends State<ScoreManager> {
int _score = 5;
void _increment() => setState(() => _score++);
void _decrement() => setState(() => _score--);
@override
Widget build(BuildContext context) {
return ScoreProvider(
score: _score,
onIncrement: _increment,
onDecrement: _decrement,
child: widget.child,
);
}
}-
Học viên: paste/run/hot reload.
-
Kiểm tra: đảm bảo runApp(ScoreManager(child: MyApp())) hoặc tương đương.
-
Xử lý lỗi: nếu provider không ở scope, hướng dẫn cách bọc đúng.
4.4 Viết
ScoreDisplayvàScoreActionButtons(0:30–0:36) — 6 phút -
Mục tiêu: Học viên lấy data và gọi callbacks.
-
Lời dẫn mẫu:
“ScoreDisplay lấy provider trong build; ScoreActionButtons gọi provider.onIncrement/onDecrement.”
-
Code mẫu:
class ScoreDisplay extends StatelessWidget {
const ScoreDisplay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final provider = ScoreProvider.of(context);
final score = provider?.score ?? 0;
debugPrint('ScoreDisplay build: $score');
return Text('Điểm hiện tại: $score', style: TextStyle(fontSize: 28));
}
}
class ScoreActionButtons extends StatelessWidget {
const ScoreActionButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final provider = ScoreProvider.of(context);
debugPrint('ScoreActionButtons build');
return Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(onPressed: provider?.onDecrement, child: Text('-')),
SizedBox(width: 12),
ElevatedButton(onPressed: provider?.onIncrement, child: Text('+')),
],
);
}
}-
Học viên: thêm debugPrint nếu cần, chạy.
-
Kiểm tra nhanh: hỏi “ScoreDisplay lấy provider bằng method nào?” →
ScoreProvider.of(context). -
Xử lý lỗi: provider null → check scope.
4.5 Khởi động app, tương tác & quan sát console (0:36–0:42) — 6 phút
-
Mục tiêu: Học viên thấy evidence (console) cho rebuild.
-
Lời dẫn mẫu:
“Mọi người mở console, bấm +, khi thấy debugPrint từ ScoreDisplay, hô to ‘I see rebuild’.”
-
Học viên: tương tác, hô to.
-
Kiểm tra: giảng viên yêu cầu 2–3 bạn mô tả console output.
-
Xử lý lỗi: kiểm tra chạy debug, filter console.
4.6 Bài tập minh họa
capture-initial(didChangeDependencies) (0:42–0:46) — 4 phút -
Mục tiêu: Phân biệt capture once vs dependent.
-
Lời dẫn mẫu:
“Tạo
LabelInitialValuevà capture giá trị ban đầu trongdidChangeDependencies. Khi score thay đổi, widget này sẽ không tự cập nhật — đây là lý do vì sao gọiof(context)trongbuildkháccapture once.” -
Code mẫu:
class LabelInitialValue extends StatefulWidget {
const LabelInitialValue({Key? key}) : super(key: key);
@override
State<LabelInitialValue> createState() => _LabelInitialValueState();
}
class _LabelInitialValueState extends State<LabelInitialValue> {
late final int initialValue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final provider = ScoreProvider.of(context);
initialValue = provider?.score ?? 0;
debugPrint('LabelInitialValue captured: $initialValue');
}
@override
Widget build(BuildContext context) {
debugPrint('LabelInitialValue build: $initialValue');
return Text('Initial captured: $initialValue');
}
}-
Học viên: thêm widget, chạy, bấm +, quan sát behavior.
-
Kiểm tra nhanh: một học viên giải thích lý do widget không thay đổi.
-
Xử lý lỗi: nếu
didChangeDependencieskhông nhận provider, kiểm tra scope.4.7 Mini-challenge & thảo luận (0:46–0:47) — 1 phút
-
Thách thức: “Ai thêm được nút Reset (score=0) hoặc chế độ đặc biệt sẽ trình bày 30s.”
-
Mục đích: Củng cố, kích hoạt sáng tạo.
- TỔNG KẾT (Lượng hóa kiến thức đạt được, 0:47–0:50)
- Lời dẫn mẫu:
“Kết thúc: hôm nay các em đã: (1) Viết ScoreProvider & ScoreManager, (2) Hiểu vị trí state trong cây widget, (3) Phân biệt lấy trong build vs capture once. Thang đánh giá: thực hành 60%, phân tích 30%, đánh giá 10%.”
- Hướng dẫn bài tập về nhà:
- Refactor sang
ChangeNotifier+providerpackage hoặc - Mở rộng: danh sách nhiều sinh viên, mỗi sinh viên có điểm riêng.
- Refactor sang
- PHIẾU PHÁT TAY / CHEAT‑SHEET 1 TRANG
- Tiêu đề: Quản lý điểm với InheritedWidget — Nhớ 3 điều
- ScoreProvider = hộp dữ liệu đặt ở ancestor.
- Không sửa hộp trực tiếp — thay hộp mới bằng
setStateở widget bọc (ScoreManager). of(context)trongbuild()đăng ký phụ thuộc → widget con tự cập nhật.
- Mẹo nhanh:
- Nếu
of(context)trảnull: kiểm tra provider bọc đúng scope chưa. - Không muốn widget tự cập nhật: lấy giá trị 1 lần (capture in
didChangeDependencies).
- Nếu
- TÀI NGUYÊN & BƯỚC TIẾP THEO
- Gợi ý đọc thêm:
- Flutter docs: InheritedWidget
- package
provider(flutter.dev)
- Bước tiếp theo đề xuất:
- Soạn slide 1 trang (tóm tắt + sơ đồ + 3 takeaway).
- Soạn tài liệu hướng dẫn thực hành (một trang copy/paste code).
- Viết rubic chấm điểm chi tiết.
PHỤ LỤC: CODE MẪU (FULL main.dart)
Bạn có thể copy toàn bộ đoạn dưới vào lib/main.dart và chạy trực tiếp.
import 'package:flutter/material.dart';
void main() => runApp(const ScoreManager(child: MyApp()));
/// ScoreProvider: InheritedWidget đơn giản truyền score và callback xuống subtree.
class ScoreProvider extends InheritedWidget {
final int score;
final VoidCallback? onIncrement;
final VoidCallback? onDecrement;
const ScoreProvider({
Key? key,
required this.score,
this.onIncrement,
this.onDecrement,
required Widget child,
}) : super(key: key, child: child);
static ScoreProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ScoreProvider>();
}
@override
bool updateShouldNotify(covariant ScoreProvider oldWidget) {
return oldWidget.score != score;
}
}
/// ScoreManager: widget bọc có state quản lý score và tạo ScoreProvider.
class ScoreManager extends StatefulWidget {
final Widget child;
const ScoreManager({Key? key, required this.child}) : super(key: key);
@override
State<ScoreManager> createState() => _ScoreManagerState();
}
class _ScoreManagerState extends State<ScoreManager> {
int _score = 5;
void _increment() => setState(() => _score++);
void _decrement() => setState(() => _score--);
void _reset() => setState(() => _score = 0);
@override
Widget build(BuildContext context) {
return ScoreProvider(
score: _score,
onIncrement: _increment,
onDecrement: _decrement,
child: widget.child,
);
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ScoreProvider Demo',
home: const StudentScorePage(),
debugShowCheckedModeBanner: false,
);
}
}
class StudentScorePage extends StatelessWidget {
const StudentScorePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
debugPrint('StudentScorePage build');
return Scaffold(
appBar: AppBar(title: const Text('Quản lý điểm sinh viên')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
StudentNameText(name: 'Nguyễn Văn A'),
SizedBox(height: 12),
ScoreDisplay(),
SizedBox(height: 12),
LabelInitialValue(),
SizedBox(height: 12),
ScoreActionButtons(),
],
),
),
),
);
}
}
class StudentNameText extends StatelessWidget {
final String name;
const StudentNameText({Key? key, required this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold));
}
}
class ScoreDisplay extends StatelessWidget {
const ScoreDisplay({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final provider = ScoreProvider.of(context);
final score = provider?.score ?? 0;
debugPrint('ScoreDisplay build: $score');
return Text('Điểm hiện tại: $score', style: const TextStyle(fontSize: 28));
}
}
class ScoreActionButtons extends StatelessWidget {
const ScoreActionButtons({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final provider = ScoreProvider.of(context);
debugPrint('ScoreActionButtons build');
return Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(onPressed: provider?.onDecrement, child: const Text('-')),
const SizedBox(width: 12),
ElevatedButton(onPressed: provider?.onIncrement, child: const Text('+')),
const SizedBox(width: 12),
ElevatedButton(onPressed: null, child: const Text('Reset (demo)')), // Optional: wire to provider.reset if added
],
);
}
}
/// Widget minh hoạ capture-initial: lấy giá trị một lần trong didChangeDependencies
class LabelInitialValue extends StatefulWidget {
const LabelInitialValue({Key? key}) : super(key: key);
@override
State<LabelInitialValue> createState() => _LabelInitialValueState();
}
class _LabelInitialValueState extends State<LabelInitialValue> {
late final int initialValue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final provider = ScoreProvider.of(context);
initialValue = provider?.score ?? 0;
debugPrint('LabelInitialValue captured: $initialValue');
}
@override
Widget build(BuildContext context) {
debugPrint('LabelInitialValue build: $initialValue');
return Text('Initial captured: $initialValue', style: const TextStyle(fontSize: 16));
}
}LỖI THƯỜNG GẶP & GIẢI PHÁP NHANH
ScoreProvider.of(context)trảnull→ kiểm tra ScoreManager có bọc đúng scope (ví dụrunApp(const ScoreManager(child: MyApp()))) hay không.- Widget con không rebuild → kiểm tra bạn gọi
of(context)trongbuild, không phảiinitState. DùngdidChangeDependenciesnếu cần capture once. - Không thấy
debugPrint→ chạy app ở Debug mode và mở console/Logcat.
PHÁT TAY (1 trang) — in ngay cho học viên
- Tiêu đề: Quản lý điểm với InheritedWidget — 3 điều cần nhớ:
- ScoreProvider = hộp dữ liệu đặt ở ancestor.
- Đổi dữ liệu = tạo provider mới bằng
setStateở widget bọc (ScoreManager). of(context)trongbuild()đăng ký phụ thuộc → con tự cập nhật.