Một danh sách có ba bản ghi cần hai khoảng phân cách ở giữa. Nếu trộn separator vào cùng mảng dữ liệu hoặc nhân đôi itemCount khi dùng ListView.separated, bạn dễ đọc sai index và truy cập vượt phần tử cuối.
Bài đối chiếu Flutter ListView.separated API ngày 04/11/2026. Demo được rà thủ công về item count, chỉ số và constraint. Chưa build Flutter, chạy widget test hoặc thiết bị; các case UI dưới đây là kế hoạch kiểm tra, không phải kết quả render đã quan sát.
Hai builder nhận index theo dữ liệu
itemCount là số item dữ liệu. itemBuilder dùng index để lấy phần tử tương ứng. separatorBuilder tạo vùng phân cách giữa hai item: separator index 0 nằm sau item 0, và separator cuối nằm trước item cuối.
Với n item lớn hơn 0, số vị trí separator là n trừ 1; n bằng 0 thì không có separator. Đây là số vị trí trong cấu trúc danh sách, không phải số lần callback chắc chắn được gọi khi bạn cuộn, vì việc build còn phụ thuộc viewport và lifecycle.
Demo đổi giữa 0, 1 và 3 item
Có thể đặt ví dụ sau trong lib/main.dart của project Material thử:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: SeparatorDemo()));
class SeparatorDemo extends StatefulWidget {
const SeparatorDemo({super.key});
@override
State<SeparatorDemo> createState() => _SeparatorDemoState();
}
class _SeparatorDemoState extends State<SeparatorDemo> {
final names = const ['An', 'Bình', 'Chi'];
int visibleCount = 3;
@override
Widget build(BuildContext context) {
final visible = names.take(visibleCount).toList();
return Scaffold(
appBar: AppBar(title: const Text('Item và separator')),
body: Column(
children: [
Wrap(
spacing: 8,
children: [0, 1, 3].map((count) => TextButton(
onPressed: () => setState(() => visibleCount = count),
child: Text('$count item'),
)).toList(),
),
Expanded(
child: ListView.separated(
itemCount: visible.length,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) => ListTile(
title: Text(visible[index]),
subtitle: Text('Item index $index'),
),
separatorBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text('Giữa item $index và ${index + 1}'),
),
),
),
],
),
);
}
}
Column chứa phần điều khiển và Expanded cho ListView một vùng cao hữu hạn. Danh sách visible được tạo từ dữ liệu demo cố định; các nút chỉ chọn số phần tử hiển thị. Không có request, xóa dữ liệu hoặc state riêng trong từng row để kiểm chứng thêm.
Separator trong demo dùng Text để nhìn rõ index. Khi tích hợp giao diện thật, bạn có thể thay bằng SizedBox hoặc Divider theo thiết kế mà không đổi itemCount. Phần padding đầu và cuối thuộc ListView, không cần tạo item giả chỉ để có lề ngoài.
Kiểm tra từng trường hợp
Ở chế độ ba item, kiểm tra hai vùng nằm giữa 0–1 và 1–2. Với một item, không có vùng giữa các item. Với không item, danh sách không tạo một separator mồ côi. Những điều này cần xác nhận khi chạy demo trên project của bạn.
Không dùng index của separator để truy cập phần tử index + 1 mà quên điều kiện dữ liệu khác khi tùy biến. Trong cấu trúc hiện tại, callback separator chỉ áp dụng vào các vị trí có cặp kế tiếp theo API; nếu bạn tự gộp builder hoặc đổi loại list, cần xem lại hợp đồng đó.
Giữ builder tập trung vào tạo widget
Đừng tăng bộ đếm nghiệp vụ, gọi API hoặc thêm phần tử vào list bên trong builder. Callback có thể được chạy lại khi framework cần dựng giao diện. Số lần gọi builder không phải số bản ghi người dùng đã xem hoặc số hành động đã hoàn thành.
Nếu row có state và dữ liệu được sắp xếp lại, hãy thiết kế key và mapping theo API tương ứng; demo tĩnh này không giải quyết đầy đủ việc giữ state theo ID. Tài liệu hiện hành cũng có các callback hỗ trợ tìm index, nên cần kiểm tra đúng API của SDK đang dùng khi bổ sung phần đó.
Tách dữ liệu khỏi separator giúp code đọc đúng đơn vị: itemCount đếm bản ghi, còn khoảng cách được tạo ở giữa. Bắt đầu bằng ba case nhỏ trước khi đưa vào danh sách có nội dung và lifecycle phức tạp hơn.


