Hiển thị danh sách dữ liệu từ máy chủ là tính năng cốt lõi của hầu hết ứng dụng. Bài này dạy ListView.builder, GridView, gọi REST API bằng gói http, chuyển JSON thành đối tượng và hiển thị bằng FutureBuilder.
- Hiển thị danh sách dài hiệu quả với ListView.builder
- Gọi API GET/POST bằng gói http
- Chuyển JSON sang model Dart
- Xử lý trạng thái đang tải, lỗi, kéo để làm mới
- Lazy list
- Chỉ dựng phần tử đang hiển thị trên màn hình.
- REST API
- Giao diện dữ liệu qua HTTP: GET, POST, PUT, DELETE.
- FutureBuilder
- Widget dựng giao diện theo trạng thái của Future.
- Model
- Lớp Dart mô tả một đối tượng dữ liệu.
Khái niệm và lý thuyết
ListView.builder tạo phần tử theo yêu cầu khi cuộn tới, nên danh sách 10.000 dòng vẫn mượt. Dữ liệu API trả về dạng JSON; dùng jsonDecode thành Map/List, rồi chuyển sang model có kiểu để tránh lỗi gõ sai khóa.
Một màn hình tải dữ liệu luôn có ít nhất ba trạng thái: đang tải, thành công, lỗi (và đôi khi rỗng). Thiết kế đủ các trạng thái này là dấu hiệu của ứng dụng chuyên nghiệp.
Model và hàm gọi API
flutter pub add httpimport 'dart:convert';
import 'package:http/http.dart' as http;
class BaiViet {
final int id; final String tieuDe; final String noiDung;
BaiViet({required this.id, required this.tieuDe, required this.noiDung});
factory BaiViet.fromJson(Map<String, dynamic> j) =>
BaiViet(id: j['id'], tieuDe: j['title'], noiDung: j['body']);
}
Future<List<BaiViet>> taiBaiViet() async {
final res = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (res.statusCode != 200) throw Exception('Lỗi ${res.statusCode}');
final List data = jsonDecode(res.body);
return data.map((e) => BaiViet.fromJson(e)).toList();
}Hiển thị với FutureBuilder
class DanhSach extends StatefulWidget {
const DanhSach({super.key});
@override
State<DanhSach> createState() => _DanhSachState();
}
class _DanhSachState extends State<DanhSach> {
late Future<List<BaiViet>> _f = taiBaiViet(); // tạo một lần, không đặt trong build
@override
Widget build(BuildContext context) => FutureBuilder(
future: _f,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) return Center(child: Text('Lỗi: ${snap.error}'));
final ds = snap.data!;
return RefreshIndicator(
onRefresh: () async { setState(() => _f = taiBaiViet()); await _f; },
child: ListView.separated(
itemCount: ds.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => ListTile(
leading: CircleAvatar(child: Text('${ds[i].id}')),
title: Text(ds[i].tieuDe, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(ds[i].noiDung, maxLines: 2)),
),
);
},
);
}Gửi dữ liệu (POST)
final res = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': 'Xin chào', 'body': 'Nội dung', 'userId': 1}),
);Lưới ảnh dùng GridView.builder với SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 200) để tự tính số cột theo màn hình.
Android cần quyền Internet trong android/app/src/main/AndroidManifest.xml (bản release): <uses-permission android:name="android.permission.INTERNET"/>.
Đặt taiBaiViet() trực tiếp trong tham số future của build sẽ gọi API lại mỗi lần build. Luôn lưu Future vào biến State.
- Hiển thị danh sách người dùng từ /users với tên, email.
- Bấm vào một người mở màn hình chi tiết (Bài 8).
- Thêm ô tìm kiếm lọc theo tên.
- Làm màn hình lưới ảnh từ /photos với GridView.builder.
- ListView.builder dựng phần tử theo yêu cầu.
- http + jsonDecode + model để làm việc với API.
- FutureBuilder xử lý đang tải, lỗi, dữ liệu.
- Luôn thiết kế đủ trạng thái và kéo để làm mới.