Mình có tạo 1 class dùng để hiển thị 1 danh sách nào đó và để chọn - lọc dữ liệu
Trước tiên bạn cần import thư viện này vào project của bạn trước

[Thư viện diacritic](https://pub.dev/packages/diacritic

import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';

typedef GetTitle<T> = String Function(T data);
typedef ChoiceDialogTitleBuilder<T> = Widget Function(T data);
typedef OnAccept<T> = void Function(List<T>);
typedef CanCheckItem<T> = bool Function(T data);

// ignore: must_be_immutable
class ChoiceDialog<T> extends StatefulWidget {
  BuildContext context;
  List<T> searchedData = [];
  List<T> rootData = [];
  List<T>? selectedObject = [];
  List<T>? backupSelectedObject = [];
  String? title;
  String? hintSearchText;
  String choiceButtonText;
  Widget? cancelButton;
  GetTitle<T>? getTitle;
  bool isSingleChoice;
  OnAccept<T>? onAccept;
  ChoiceDialogTitleBuilder<T>? itemBuilder;
  bool isShowCheckAllWidget = false;
  CanCheckItem<T>? isCanCheckItem;
  ChoiceDialog(this.context, this.searchedData,
      {Key? key,
      List<T>? selectedObject,
      this.title,
      this.choiceButtonText = "Chọn",
      @required this.getTitle,
      this.isSingleChoice = true,
      this.onAccept,
      this.isShowCheckAllWidget = false,
      this.hintSearchText,
      this.isCanCheckItem,
      this.itemBuilder})
      : super(key: key) {
    rootData = searchedData;
    if (isNotNullOrEmpty(selectedObject)) {
      this.selectedObject = selectedObject;
      backupSelectedObject?.addAll(selectedObject!);
    }
  }

  Future<List<T>> showChoiceDialog() async {
    return await pushPage(context, this);
  }

  @override
  State<StatefulWidget> createState() {
    return _ChoiceDialogState<T>();
  }
}

class _ChoiceDialogState<T> extends State<ChoiceDialog<T>> {
  List<T> searchedData = [];
  List<T> rootData = [];
  List<T> selectedObject = [];
  List<T> backupSelectedObject = [];
  String? title;
  String? hintSearchText;
  String? choiceButtonText;
  GetTitle<T>? getTitle;
  bool? isSingleChoice;
  OnAccept<T>? onAccept;
  ChoiceDialogTitleBuilder<T>? itemBuilder;
  CanCheckItem<T>? isCanCheckItem;

  bool isCheckAll = false;

  @override
  void initState() {
    super.initState();
    searchedData = widget.searchedData;
    rootData = widget.rootData;
    selectedObject.addAll(widget.selectedObject ?? []);
    title = widget.title;
    hintSearchText = widget.hintSearchText;
    choiceButtonText = widget.choiceButtonText;
    getTitle = widget.getTitle;
    isSingleChoice = widget.isSingleChoice;
    onAccept = widget.onAccept;
    itemBuilder = widget.itemBuilder;
    backupSelectedObject = [];
    backupSelectedObject.addAll(selectedObject);
    isCheckAll = searchedData.length == selectedObject.length;
    isCanCheckItem = widget.isCanCheckItem;
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () {
        Navigator.pop(context, backupSelectedObject);
        return Future.value(false);
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(title ?? ""),
          actions: [
            Visibility(
              visible: widget.isShowCheckAllWidget,
              child: IconButton(
                onPressed: () {
                  setState(
                    () {
                      isCheckAll = !isCheckAll;
                      if (isCheckAll) {
                        selectedObject.addAll(searchedData);
                      } else {
                        selectedObject.clear();
                      }
                    },
                  );
                },
                icon: Padding(
                    padding: const EdgeInsets.only(right: 16),
                    child: getIcon(0, isCheckAll: isCheckAll)),
              ),
            ),
          ],
        ),
        body: SafeArea(
          child: Column(
            children: [
              Visibility(
                visible: hintSearchText != null,
                child: Container(
                  margin:
                      const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                  decoration: BoxDecoration(
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
                      color: getColor('EFF0F5')),
                  child: TextField(
                    onChanged: (value) {
                      var text = removeDiacritics(value).toLowerCase();
                      searchedData = rootData.where((element) {
                        String title = getTitle!(element);
                        if (title == null) return false;
                        return removeDiacritics(title)
                            .toLowerCase()
                            .contains(text);
                      }).toList();
                      setState(() {});
                    },
                    decoration: InputDecoration(
                      contentPadding: const EdgeInsets.all(8),
                      prefixIconConstraints:
                          const BoxConstraints(maxHeight: 32, maxWidth: 32),
                      isDense: true,
                      border: InputBorder.none,
                      hintText: hintSearchText,
                      prefixIcon: const Padding(
                        padding: EdgeInsets.symmetric(horizontal: 8),
                        child: Icon(
                          Icons.search,
                          size: 20,
                        ),
                      ),
                    ),
                  ),
                ),
              ),
              Container(
                color: getColor('EFF0F5'),
                height: 1,
                margin: EdgeInsets.zero,
              ),
              Expanded(
                  child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 16),
                      child: _getWidgetContent())),
              Container(
                color: getColor('EFF0F5'),
                height: 12,
                margin: const EdgeInsets.symmetric(vertical: 12),
              ),
              saveButton(
                titleButton: choiceButtonText ?? "Xong",
                onClickButton: () async {
                  Navigator.pop(context, selectedObject);
                  if (onAccept != null) {
                    onAccept!(selectedObject);
                  }
                },
              ),
              saveButton(
                colorBackground: Colors.grey,
                titleButton: "Hủy",
                onClickButton: () async {
                  Navigator.pop(context, backupSelectedObject);
                },
              )
            ],
          ),
        ),
      ),
    );
  }

  Widget saveButton({
    required String titleButton,
    required VoidCallback onClickButton,
    Color? colorBackground,
  }) {
    return GestureDetector(
      onTap: onClickButton,
      child: Container(
        margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
        padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: colorBackground ?? Colors.blue,
          borderRadius: BorderRadius.circular(5000),
        ),
        child: Text(
          titleButton,
          style: TextStyle(color: Colors.white, fontSize: 18),
        ),
      ),
    );
  }

  Widget _getWidgetContent() {
    return isNullOrEmpty(searchedData)
        ? const Center(
            child: Text("Không có kết quả tìm kiếm"),
          )
        : ListView.builder(
            itemCount: searchedData.length,
            itemBuilder: (context, index) {
              var model = searchedData[index];
              return InkWell(
                onTap: () {
                  if (isCanCheckItem != null &&
                      isCanCheckItem!(model) == false) {
                    return;
                  }
                  if (isSingleChoice == true) {
                    if (selectedObject.contains(model)) {
                      selectedObject.remove(model);
                    } else {
                      selectedObject.clear();
                      selectedObject.add(model);
                    }
                  } else {
                    if (selectedObject.contains(model)) {
                      selectedObject.remove(model);
                    } else {
                      selectedObject.add(model);
                    }
                    isCheckAll = selectedObject.length == searchedData.length;
                    setState(() {});
                  }
                  setState(() {});
                },
                child: Container(
                  padding: const EdgeInsets.symmetric(vertical: 4),
                  child: Row(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Expanded(
                        child: itemBuilder != null
                            ? itemBuilder!(model)
                            : Text(getTitle!(model)),
                      ),
                      getIcon(index)
                    ],
                  ),
                ),
              );
            },
          );
  }

  Widget getIcon(int index, {bool? isCheckAll}) {
    bool isSelected =
        isCheckAll ?? selectedObject.contains(searchedData[index]);
    IconData iconData;
    if (isSingleChoice == true) {
      iconData = isSelected
          ? Icons.radio_button_checked_outlined
          : Icons.radio_button_off;
    } else {
      iconData = isSelected
          ? Icons.check_box_outlined
          : Icons.check_box_outline_blank_rounded;
    }
    Color color = isSelected ? Colors.blue : Colors.grey;
    return Icon(
      iconData,
      color: color,
    );
  }
}

Color getColor(String hex, {int? alpha}) {
  hex = "0xff" + hex;
  return Color(int.parse(hex));
}

bool isNullOrEmpty(dynamic object) {
  if (object == null) return true;
  if (object == "null") return true;
  if (object is String) {
    return object.isEmpty;
  }
  if (object is List) {
    return object.length == 0;
  }
  if (object is Map) {
    return object.length == 0;
  }
  if (object is Set) {
    return object.length == 0;
  }
  return false;
}

bool isNotNullOrEmpty(dynamic object) {
  return !isNullOrEmpty(object);
}

Future<T?> pushPage<T>(BuildContext context, Widget page) {
  return Navigator.of(context).push<T>(MaterialPageRoute(
      builder: (context) => page,
      settings: RouteSettings(arguments: page.toStringShort())));
}


Vậy cách sử dụng class trên như thế nào

ChoiceDialog choiceDialog = ChoiceDialog<int>(
              context,
              [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
              selectedObject: [1],
              isSingleChoice: false,
              title: "Chọn phòng",
              hintSearchText: "Nhập tên phòng",
              getTitle: (data) => "$data",
              choiceButtonText: "Chọn phòng ban",
              isShowCheckAllWidget: true,
              isCanCheckItem: (selected) {
                print('isCanCheckItem.build $selected');
                if (selected == 2) {
                  return false;
                }
                return true;
              },
              itemBuilder: (data) {
                return Text(
                  data.toString() ?? "",
                  style: TextStyle(fontSize: 14),
                );
              },
              onAccept: (selected) {
                print('_DemoDialogState.build $selected');
              },
            );
            choiceDialog.showChoiceDialog();

  • Trước tiên cần truyền vào 1 kiểu dữ liệu: Có thể sử dụng bất kỳ kiểu dữ liệu nào

  • Bắt buộc cần truyền context và list

  • title: tiêu đề Appbar

  • isSingleChoice: true là chọn 1, false là chọn nhiều

  • hintSearchText: có thể tìm kiếm dữ liệu trong danh sách

  • getTitle: dữ liệu cần dc hiển thị ở danh sách là trường nào

  • selectedObject: cần truyền 1 danh sách dữ liệu đã chọn

  • choiceButtonText: tên của button lưu

  • isShowCheckAllWidget: true là hiện thị checkbox , false là ẩn, chức năng dùng để chọn - bỏ tất cả dữ liệu

  • isCanCheckItem: điều kiện để không cho chọn 1 dữ liệu nào đó trong list

  • itemBuilder: mỗi item hiển thị như nào đều code UI ở đây

  • onAccept: khi chọn dữ liệu xong thì truyền dữ liệu đã chọn ra ngoài

    Ví dụ code trên của mình

    • Kiểu dữ liệu là int
    • list là [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
    • getTitle: hiển thị dữ liệu ra mỗi item
    • selectedObject dữ liệu trước đó mình đã chọn, khi chọn lại nó sẽ hiển thị đã chọn lên
    • isCanCheckItem: mình đang đặt điều kiện là nếu dữ liệu = 2 sẽ không cho chọn và các dữ liệu khác sẽ được chọn
    • itemBuilder: đây là giao diện mỗi item của mình
    • onAccept: khi mình chọn dữ liệu xong sẽ truyền ra ngoài
    • Image 1
      )