Bạn có một màn hình đọc danh sách và nhận thấy tác vụ bất đồng bộ chạy lại sau khi widget cha cập nhật. Một nguyên nhân cần kiểm tra là future: loadItems() đang nằm ngay trong build.
Bài này dành cho người đã biết StatefulWidget, Future và setState. Ví dụ dùng dữ liệu giả để bạn tập trung vào vòng đời widget, không cần một server. Các hành vi được đối chiếu tài liệu Flutter ngày 21/10/2026.
1. Vì sao lời gọi trong build gây rắc rối?
FutureBuilder quan sát một Future; nó không quản lý giúp bạn chính sách lúc nào nên tạo tác vụ mới. Tài liệu FutureBuilder yêu cầu lấy Future trước, chẳng hạn trong initState, thay vì tạo cùng lúc xây widget trong build.
Hãy xem đoạn dễ gây hiểu nhầm này:
FutureBuilder<List<String>>(
future: loadItems(),
builder: (context, snapshot) {
return Text('${snapshot.data?.length ?? 0} mục');
},
)
Mỗi lần đoạn này được dựng lại, loadItems() có thể tạo một Future mới và khởi động công việc mới. Việc rebuild UI vốn là hoạt động bình thường; bạn cần tách nó khỏi quyết định tải dữ liệu.
2. Giữ Future trong State
Ví dụ dưới đây là một màn hình độc lập. Bạn có thể đặt nó trong một dự án Flutter sẵn có và mở bằng MaterialApp(home: TopicsPage()).
import 'package:flutter/material.dart';
class TopicsPage extends StatefulWidget {
const TopicsPage({super.key});
@override
State<TopicsPage> createState() => _TopicsPageState();
}
class _TopicsPageState extends State<TopicsPage> {
late Future<List<String>> _topics;
Future<List<String>> _loadTopics() async {
await Future<void>.delayed(const Duration(milliseconds: 500));
return ['Flutter', 'Dart', 'Widget lifecycle'];
}
@override
void initState() {
super.initState();
_topics = _loadTopics();
}
void _reload() {
setState(() {
_topics = _loadTopics();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Chủ đề học tập')),
body: FutureBuilder<List<String>>(
future: _topics,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: TextButton(
onPressed: _reload,
child: const Text('Tải lỗi. Thử lại'),
),
);
}
final topics = snapshot.data ?? const <String>[];
if (topics.isEmpty) {
return const Center(child: Text('Chưa có chủ đề'));
}
return ListView.builder(
itemCount: topics.length,
itemBuilder: (context, index) {
return ListTile(title: Text(topics[index]));
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _reload,
child: const Icon(Icons.refresh),
),
);
}
}
initState khởi tạo tác vụ cho State hiện tại. Nút tải lại là một sự kiện rõ ràng để thay Future. setState nhận callback đồng bộ; công việc chờ diễn ra trong _loadTopics, không biến callback thành async. Xem initState và setState.
3. Xử lý khi dữ liệu đầu vào thay đổi
Nếu màn hình nhận userId, tạo Future một lần trong initState chưa đủ: cùng một State có thể nhận widget mới với ID khác. Khi đó, so sánh ID cũ và mới trong didUpdateWidget, rồi thay Future nếu cần.
@override
void didUpdateWidget(covariant UserPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
_user = loadUser(widget.userId);
}
}
Đoạn trên là mẫu ghép vào một UserPage có các trường tương ứng, không phải phần bổ sung trực tiếp cho TopicsPage. Theo tài liệu didUpdateWidget, framework sẽ gọi build sau callback này, nên riêng phép thay Future ở đây không cần thêm setState.
4. Kiểm tra hành vi bằng những thay đổi nhỏ
Thêm log vào _loadTopics để đếm lần bắt đầu tác vụ. Mở màn hình, gây rebuild từ widget cha và quan sát xem số lần tải có tăng ngoài ý muốn không. Sau đó bấm refresh và xác nhận có đúng một tác vụ mới cho sự kiện đó.
Tiếp tục đổi hàm tải để trả danh sách rỗng hoặc ném Exception('demo'). Bạn cần thấy được ba trạng thái riêng: đang tải, tải lỗi và tải thành công nhưng chưa có mục nào. Đây là các bước kiểm tra đề xuất; hãy chạy flutter analyze và widget test trên SDK của dự án trước khi tích hợp.
Lỗi thường gặp
- Nháy loading khi refresh: ví dụ cố ý hiển thị spinner cho Future mới. Nếu muốn giữ dữ liệu cũ, cần thiết kế UI thể hiện “đang làm mới” riêng.
- Dữ liệu cũ sau khi đổi tài khoản: kiểm tra đầu vào đã đổi mà Future vẫn giữ từ
initStatehay chưa. - Cho rằng FutureBuilder tự hủy request cũ: đổi Future chỉ thay thứ được widget quan sát; khả năng hủy tác vụ phụ thuộc cơ chế thực hiện request.
Khi có cache, nhiều màn hình dùng chung dữ liệu hoặc cần đồng bộ phức tạp, hãy đặt việc tải vào repository hoặc lớp quản lý state thích hợp. Với một màn hình nhỏ, giữ Future ổn định và quyết định tải lại theo sự kiện là bước đầu dễ kiểm chứng.



