Màn hình có vài dòng nên chưa đủ cao để cuộn. Bạn đã bọc ListView bằng RefreshIndicator, nhưng kéo xuống vẫn không thấy vòng làm mới. Trước khi sửa API hoặc thêm state loading, hãy kiểm tra khả năng nhận thao tác kéo của chính scrollable.
Bài đối chiếu Flutter API hiện hành ngày 02/11/2026. Ví dụ đã rà thủ công theo hợp đồng widget và lifecycle; chưa build bằng Flutter SDK, chưa chạy widget test hay thiết bị. Phần danh sách, thời gian chờ và lỗi là dữ liệu mô phỏng cục bộ, không phải một lần gọi mạng đã được đo.
Hai điều kiện khác nhau
RefreshIndicator cần một scroll view theo chiều dọc và tín hiệu overscroll phù hợp. Khi nội dung ngắn hơn viewport, thêm AlwaysScrollableScrollPhysics vào ListView để vẫn cho người dùng thực hiện thao tác cuộn. Physics này không tự thay thế toàn bộ hành vi overscroll của nền tảng.
Điều kiện tiếp theo là callback onRefresh trả về Future đại diện cho công việc tải. Indicator dựa vào Future ấy để biết lúc kết thúc. Nếu gọi một hàm async rồi bỏ Future, spinner có thể kết thúc sớm hơn công việc mà người dùng đang chờ.
Demo tối thiểu với hai dòng dữ liệu
Có thể thay lib/main.dart trong project Flutter Material thử nghiệm bằng ví dụ sau:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: RefreshDemo()));
class RefreshDemo extends StatefulWidget {
const RefreshDemo({super.key});
@override
State<RefreshDemo> createState() => _RefreshDemoState();
}
class _RefreshDemoState extends State<RefreshDemo> {
int revision = 0;
bool failNext = false;
String? error;
Future<void> refresh() async {
final shouldFail = failNext;
try {
await Future<void>.delayed(const Duration(milliseconds: 500));
if (shouldFail) throw StateError('Demo failure');
if (!mounted) return;
setState(() {
revision += 1;
error = null;
});
} catch (_) {
if (!mounted) return;
setState(() => error = 'Chưa làm mới được. Hãy kéo để thử lại.');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Kéo làm mới')),
body: RefreshIndicator(
onRefresh: refresh,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
children: [
Text('Phiên dữ liệu: $revision'),
const ListTile(title: Text('Dòng minh họa')),
SwitchListTile(
title: const Text('Mô phỏng lỗi khi làm mới'),
value: failNext,
onChanged: (value) => setState(() => failNext = value),
),
if (error != null) Text(error!),
],
),
),
);
}
}
Biến shouldFail chụp cấu hình tại lúc bắt đầu lượt để việc đổi switch giữa chừng không đổi kết quả của lượt đang chờ. revision chỉ tăng khi thành công; lỗi được chuyển thành thông báo trên UI và Future vẫn hoàn thành sau xử lý. Bài không ghi nhận một lỗi mạng thật nào.
Kiểm tra theo những trạng thái có ý nghĩa
Trên project thử, kéo từ đầu danh sách ngắn và kiểm tra indicator xuất hiện; sau khi hoàn thành, revision tăng đúng một lượt. Bật mô phỏng lỗi, kéo lại: số phiên giữ nguyên, thông báo lỗi hiển thị, indicator kết thúc. Tắt lỗi rồi thử lại để kiểm tra thông báo cũ được xóa khi thành công.
Hãy thử thêm màn hình nhỏ, danh sách dài và rời màn hình trong lúc Future còn chờ. Những case này là kế hoạch kiểm tra trên thiết bị, chưa được thực thi trong bài. mounted bảo vệ lần cập nhật UI sau await; nó không hủy tác vụ nền hay request HTTP nếu bạn thay phần chờ bằng mạng thật.
Lỗi thường gặp khi tích hợp
Đặt physics trên widget bọc ngoài mà không đặt trên scrollable con sẽ không giải quyết đúng chỗ. Bọc một Column không cuộn cũng không tạo nguồn scroll notification cần thiết. Demo dùng Scaffold.body cho ListView một vùng cao hữu hạn; nếu chuyển vào Column khác, cần giữ constraint phù hợp.
Với danh sách rỗng, vẫn giữ scrollable và hiển thị empty state bên trong thay vì thay cả cây bằng một Text tĩnh. Với nested scroll, đừng sao chép nguyên demo rồi kết luận notification đúng cấp: cần xem thêm notificationPredicate theo cấu trúc thật.
Demo chỉ có một đường gọi refresh từ indicator. Nếu còn nút tải lại hoặc nguồn cập nhật khác, cần chính sách tránh các response cũ ghi đè dữ liệu mới. Giải quyết khả năng kéo và vòng đời Future trước, rồi mới bổ sung luồng tải phức tạp hơn.


