Bạn tăng bộ đếm ở một hàng rồi đảo danh sách. Nhãn của hàng di chuyển nhưng bộ đếm dường như vẫn nằm ở vị trí cũ. Nếu state được giữ local trong từng StatefulWidget, hãy xem cách framework nhận diện các hàng khi dựng lại cây.
Bài này dùng Column có hai hàng để quan sát mối liên hệ giữa dữ liệu và state. Tài liệu Flutter được đối chiếu ngày 26/10/2026. Code đã được rà theo API nhưng chưa build hoặc chạy widget test trong lượt biên tập; không có kết quả chạy thiết bị giả định trong bài.
1. Key là định danh tại một vị trí trong cây
Widget.key giải thích việc framework so sánh runtimeType và key khi cập nhật element. ValueKey dùng giá trị để nhận diện và phù hợp với dữ liệu có ID ổn định khi danh sách thay đổi.
Trong ví dụ, hai Item có ID a và b. Nhãn chỉ phục vụ hiển thị. Nếu nhãn được đổi trong tương lai, bạn vẫn muốn state gắn với cùng ID, không tự đổi sang một mục khác chỉ vì dòng chữ đã khác.
2. Một ví dụ đủ để thử đảo thứ tự
Đặt vào lib/main.dart của dự án Flutter thử:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: KeyDemo()));
class Item {
const Item(this.id, this.label);
final String id;
final String label;
}
class KeyDemo extends StatefulWidget {
const KeyDemo({super.key});
@override
State<KeyDemo> createState() => _KeyDemoState();
}
class _KeyDemoState extends State<KeyDemo> {
List<Item> _items = const [Item('a', 'Backend'), Item('b', 'Mobile')];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Key và thứ tự')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
FilledButton(
onPressed: () => setState(() {
_items = _items.reversed.toList();
}),
child: const Text('Đảo thứ tự'),
),
for (final item in _items)
CounterTile(key: ValueKey(item.id), label: item.label),
],
),
),
);
}
}
class CounterTile extends StatefulWidget {
const CounterTile({super.key, required this.label});
final String label;
@override
State<CounterTile> createState() => _CounterTileState();
}
class _CounterTileState extends State<CounterTile> {
int _count = 0;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text('${widget.label}: $_count'),
trailing: IconButton(
tooltip: 'Tăng ${widget.label}',
onPressed: () => setState(() => _count += 1),
icon: const Icon(Icons.add),
),
);
}
}
State bộ đếm nằm trong CounterTile; parent chỉ quản lý thứ tự và dữ liệu nhãn. Cách bố trí này cố ý làm tác động của key dễ quan sát. Nếu bộ đếm đã được lưu ở một store khác theo ID, tình huống cần chẩn đoán sẽ khác.
3. Thực hiện phép thử có đối chứng
Tăng Backend lên một giá trị dễ nhớ rồi bấm đảo. Với ValueKey theo ID, kỳ vọng bộ đếm tiếp tục đi cùng Backend. Sau đó thử một bản riêng bỏ đối số key và khởi động lại state của demo trước khi quan sát lần nữa.
Không trộn kết quả giữa hot reload và một phiên state mới khi bạn đang đổi cách nhận diện widget. Ghi rõ bản code, thao tác và nhãn đang giữ bộ đếm. Tài liệu ValueKey có minh họa cùng vấn đề nhận diện khi reorder; bài chưa khẳng định đã chạy đối chứng trên một SDK cụ thể.
4. Chọn key ổn định và đặt đúng cấp
ID nên ổn định, duy nhất giữa các mục cùng parent và gắn với thực thể bạn muốn giữ state. Dùng index làm key khi index đổi theo reorder sẽ không diễn đạt cùng ý định đó. Dùng nhãn có thể trùng hoặc được chỉnh sửa cũng cần cân nhắc lại hợp đồng dữ liệu.
Trong cây mẫu, CounterTile là child trực tiếp của Column nên key được đặt tại đó. Nếu sau này bọc mỗi hàng bằng một Padding ở cấp danh sách, hãy xem child nào đang được đối chiếu ở cấp đó. Chỉ đặt key sâu hơn trong một cây con không tự làm mọi ancestor nhận diện đúng các hàng.
Đừng dùng UniqueKey mới trong mỗi lần build để “bảo đảm duy nhất” khi mục tiêu là giữ định danh qua các frame. UniqueKey chỉ bằng chính nó; tạo đối tượng mới có thể khiến framework xem đó là child mới và tạo state mới.
Key không thay thế quản lý dữ liệu
ValueKey không tự lưu state qua việc ứng dụng bị hủy process hay đồng bộ dữ liệu với server. Nó cũng không chữa việc hai bản ghi dùng cùng ID hoặc parent cung cấp sai dữ liệu. Hãy xác định phạm vi vấn đề trước: nhận diện element, vòng đời màn hình hay lưu trữ lâu dài.
Với danh sách dựng lười hoặc nhiều cấp state phức tạp hơn Column, cần đọc thêm API của widget cụ thể. Không suy ra rằng thêm một key bất kỳ sẽ giữ mọi cây con tồn tại vô thời hạn ngoài vùng hiển thị.
Kiểm tra và lỗi thường gặp
Thử đảo hai lần, thêm một mục mới và đổi nhãn của một ID trong bản demo mở rộng. State mong đợi cần được mô tả theo ID. Nếu hai hàng trao đổi bộ đếm, rà key ở cấp sibling; nếu bộ đếm luôn về 0, tìm key bị tạo lại hoặc child bị loại khỏi cây.
Kiểm tra duplicate key trước khi tìm mẹo tối ưu rebuild. Một thông báo key trùng thường là tín hiệu dữ liệu hoặc cách gắn key cần sửa, không phải lý do chuyển sang key ngẫu nhiên để bỏ qua lỗi.
Kết luận
Key ổn định giúp framework duy trì mối liên hệ giữa element và dữ liệu khi cấu trúc thay đổi. Bắt đầu bằng một phép thử hai hàng rồi đặt key theo ID ở đúng cấp, thay vì thêm key như một thuộc tính trang trí.

