Bài 9. Danh sách và gọi API

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.

60 phútTrung cấp
Bạn sẽ học được
  • 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
Thuật ngữ cần biết
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 http
import '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.

Mẹo hay

Android cần quyền Internet trong android/app/src/main/AndroidManifest.xml (bản release): <uses-permission android:name="android.permission.INTERNET"/>.

Lưu ý

Đặ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.

Thực hành
  1. Hiển thị danh sách người dùng từ /users với tên, email.
  2. Bấm vào một người mở màn hình chi tiết (Bài 8).
  3. Thêm ô tìm kiếm lọc theo tên.
  4. Làm màn hình lưới ảnh từ /photos với GridView.builder.
Tóm tắt bài học
  • 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.