Skip to content

Latest commit

 

History

History
1027 lines (839 loc) · 32.5 KB

File metadata and controls

1027 lines (839 loc) · 32.5 KB

BÀI GIẢNG THỰC HÀNH - INHERITED WIDGET

Quản lý điểm sinh viên với InheritedWidget


SLIDE 1: XÁC ĐỊNH VỊ TRÍ TRẠNG THÁI TRONG CÂY WIDGET

Chủ đích: Hiểu vị trí đặt state trong widget tree để chia sẻ dữ liệu hiệu quả

🎯 Mục tiêu

  • 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

� Vấn đề: Cây Widget hiện tại (Prop Drilling)

MaterialApp
  └── HomeScreen
      ├── StudentList
      │   └── StudentCard (cần đọc students)
      └── OverviewCard (cần đọc average)

ScoreEntryScreen (cần gọi setScore)

Vấn đề:

  • StudentList cần danh sách sinh viên
  • OverviewCard cần điểm trung bình
  • ScoreEntryScreen cầ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!

✅ Giải pháp: Đặt state ở ancestor chung

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

🔍 Phân tích vị trí

Tại sao đặt ở GradeProvider?

  1. Ancestor chung của tất cả widgets cần data
  2. Bọc MaterialApp → toàn bộ app có thể truy cập
  3. Không cần props → widgets con tự lấy dữ liệu

So sánh với các vị trí khác

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 ⚠️ Có thể nhưng không tối ưu
GradeProvider wrap MaterialApp Phạm vi toàn app + tách logic Cần thêm 1 layer ✅ Tối ưu nhất

📁 Cấu trúc dự án

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

💡 Model Student (Setup ban đầu)

// 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,
  });
}

📊 Sơ đồ luồng dữ liệu

┌─────────────────────────────────────┐
│     GradeProvider (State Here)      │
│  • List<Student> students           │
│  • double average                   │
│  • setScore(id, score)              │
└──────────────┬──────────────────────┘
               │
      ┌────────┼────────┐
      │        │        │
  ┌───▼───┐ ┌──▼──┐ ┌──▼─────────────┐
  │ List  │ │ Card│ │ ScoreEntry     │
  │ Widget│ │Widget│ │ Screen        │
  └───────┘ └─────┘ └────────────────┘
  (đọc)     (đọc)   (đọc + ghi)

❓ Câu hỏi kiểm tra

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

✅ Checkpoint

  • 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.dart chạy không lỗi

SLIDE 2: XÂY DỰNG LỚP PROVIDER KẾ THỪA INHERITEDWIDGET

Chủ đích: Tạo lớp Provider kết hợp StatefulWidget + InheritedWidget để quản lý state

🎯 Mục tiêu

  • 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

🏗️ Kiến trúc 2 lớp Provider

┌──────────────────────────────────────┐
│   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        │
└──────────────────────────────────────┘

📝 Bước 1: StatefulWidget Wrapper (Chứa State)

// 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

📝 Bước 2: State Class (Logic nghiệp vụ)

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ại GradeInherited

📝 Bước 3: InheritedWidget (Chia sẻ dữ liệu)

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:

1. dependOnInheritedWidgetOfExactType<T>()

// Flutter tìm InheritedWidget gần nhất có type GradeInherited
// và TỰ ĐỘNG đăng ký phụ thuộc (dependency)
context.dependOnInheritedWidgetOfExactType<GradeInherited>()

2. updateShouldNotify()

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

🔍 So sánh 2 cách kiểm tra thay đổi

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

📊 Sơ đồ luồng rebuild

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

❓ Câu hỏi kiểm tra

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

✅ Checkpoint

  • Hiểu vai trò của StatefulWidget (chứa state)
  • Hiểu vai trò của InheritedWidget (chia sẻ state)
  • Code grade_provider.dart chạy không lỗi
  • Hiểu updateShouldNotify() hoạt động thế nào
  • Hiểu tại sao dùng version thay 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

✅ Checkpoint

  • GradeProvider chạy không lỗi
  • Hiểu vai trò của version
  • Hiểu updateShouldNotify() hoạt động thế nào

SLIDE 3: TRUY XUẤT DỮ LIỆU TỪ PROVIDER TẠI WIDGET CON

Chủ đích: Sử dụng Provider.of(context) để đọc dữ liệu và tự động rebuild

🎯 Mục tiêu

  • 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

� Cơ chế truy xuất dữ liệu

Bước 1: Gọi of(context) trong build()

// 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ước 2: Flutter đăng ký phụ thuộc tự động

// 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:

  1. Flutter tìm GradeInherited gần nhất trong ancestor tree
  2. Đăng ký widget hiện tại vào danh sách phụ thuộc của GradeInherited
  3. Khi GradeInherited rebuild + updateShouldNotify() return true → widget này tự động rebuild

📋 Ví dụ 1: StudentList (Widget đọc danh sách)

// 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

📊 Ví dụ 2: OverviewCard (Widget đọc điểm trung bình)

// 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

🔄 Sơ đồ luồ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)      │
└──────────────┘  └────────────────┘

❓ Câu hỏi kiểm tra

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

✅ Checkpoint

  • 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

SLIDE 4: THAY ĐỔI TRẠNG THÁI ĐỂ CẬP NHẬT GIAO DIỆN

Chủ đích: Gọi methods từ Provider để thay đổi state và trigger UI update

🎯 Mục tiêu

  • 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

🔄 Cơ chế thay đổi trạng thái

Quy trình cập nhật state

┌────────────────────┐
│  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        │
└────────────────────┘

✏️ ScoreEntryScreen (Widget thay đổi state)

// 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:

1. Đọc dữ liệu ban đầu (didChangeDependencies)

// 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;
}

2. Thu thập input từ user (local state)

// 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
  })
)

3. Gọi setScore để cập nhật global state

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ề
}

🏠 Tích hợp vào App

HomeScreen (Màn hình chính)

// 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),
        ],
      ),
    );
  }
}

main.dart (Entry point - Đặt Provider)

// 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,
      ),
    );
  }
}

📊 Luồng dữ liệu hoàn chỉnh

[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

🧪 Kịch bản test

Bước 1: Khởi động app

✓ Thấy 4 sinh viên chưa có điểm
✓ OverviewCard hiển thị average = 0.0

Bước 2: Nhập điểm sinh viên đầu tiên

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"

Bước 3: Quan sát thay đổi

✓ Avatar "Nguyễn Văn A" → Màu xanh (≥8)
✓ Progress bar xuất hiện (85%)
✓ OverviewCard: average = 8.5

Bước 4: Nhập điểm sinh viên thứ 2

1. Tap "Trần Thị B"
2. Nhập TextField: "6.0"
3. Bấm "Lưu"

Bước 5: Quan sát average tự động cập nhật

✓ Avatar "Trần Thị B" → Màu cam (5-8)
✓ OverviewCard: average = (8.5 + 6.0) / 2 = 7.25

❓ Câu hỏi kiểm tra cuối

Q1: Widget nào được rebuild khi gọi setScore()?StudentListOverviewCard (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)

🎉 Tổng kết

3 điều cần nhớ

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

🔧 Troubleshooting

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)

📚 Bài tập về nhà

  1. Level 1: Thêm nút "Xóa điểm" trong ScoreEntryScreen
  2. Level 2: Thêm màn hình thống kê (số sinh viên giỏi/TB/yếu)
  3. Level 3: Refactor sang ChangeNotifier + provider package

✅ Checkpoint cuối cùng

  • 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

📖 Tài nguyên tham khảo