Flutter 分页加载列表 + 偏移量分页

107 阅读2分钟

✅ 功能概览

  • 分页加载(offset 模式)
  • 删除单项
  • 删除后补全数据
  • 若删除后当前页为空,自动回退并重新加载

📦 完整代码(可直接运行)

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Offset Paging Demo',
      home: PaginatedListPage(),
    );
  }
}

class PaginatedListPage extends StatefulWidget {
  const PaginatedListPage({super.key});

  @override
  State<PaginatedListPage> createState() => _PaginatedListPageState();
}

class _PaginatedListPageState extends State<PaginatedListPage> {
  final List<Item> _items = [];
  int _offset = 0;
  final int _pageSize = 20;
  bool _isLoading = false;
  bool _hasMore = true;

  final FakeServer _fakeServer = FakeServer();

  @override
  void initState() {
    super.initState();
    _loadPage();
  }

  Future<void> _loadPage({bool isRefresh = false}) async {
    if (_isLoading) return;

    setState(() => _isLoading = true);

    if (isRefresh) {
      _offset = 0;
      _items.clear();
      _hasMore = true;
    }

    final newItems = await fetchItems(_offset, _pageSize);

    setState(() {
      _items.addAll(newItems);
      _offset += newItems.length;
      _isLoading = false;
      if (newItems.length < _pageSize) {
        _hasMore = false;
      }
    });
  }

  Future<void> _deleteItem(String id) async {
    final success = await deleteItemApi(id);
    if (!mounted || !success) return;

    setState(() {
      _items.removeWhere((item) => item.id == id);
      _offset--; // 删除一项后 offset 向前收回一个
    });

    // 如果不足 pageSize,尝试补1条
    if (_hasMore && _items.length % _pageSize != 0) {
      final patch = await fetchItems(_offset, 1);
      if (patch.isNotEmpty) {
        setState(() {
          _items.add(patch.first);
          _offset++;
        });
      } else {
        _hasMore = false;
      }
    }

    // 当前页数据全删光了
    if (_items.isEmpty && _offset > 0) {
      final newOffset = (_offset - _pageSize).clamp(0, _fakeServer.count());
      _offset = newOffset;
      await _loadPage(isRefresh: true);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('分页 + 删除(Offset 模式)')),
      body: NotificationListener<ScrollNotification>(
        onNotification: (scrollInfo) {
          if (!_isLoading &&
              _hasMore &&
              scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
            _loadPage();
          }
          return false;
        },
        child: ListView.builder(
          itemCount: _items.length + (_hasMore ? 1 : 0),
          itemBuilder: (context, index) {
            if (index == _items.length) {
              return const Padding(
                padding: EdgeInsets.all(16),
                child: Center(child: CircularProgressIndicator()),
              );
            }

            final item = _items[index];
            return ListTile(
              title: Text(item.title),
              trailing: IconButton(
                icon: const Icon(Icons.delete),
                onPressed: () => _deleteItem(item.id),
              ),
            );
          },
        ),
      ),
    );
  }

  // ================================
  // API 接口(模拟后端)
  // ================================
  Future<List<Item>> fetchItems(int offset, int limit) async {
    await Future.delayed(const Duration(milliseconds: 400));
    return _fakeServer.getItemsByOffset(offset, limit)
        .map((e) => Item(e.id, e.title))
        .toList();
  }

  Future<bool> deleteItemApi(String id) async {
    await Future.delayed(const Duration(milliseconds: 300));
    return _fakeServer.deleteItem(id);
  }
}

// ================================
// 数据结构
// ================================
class Item {
  final String id;
  final String title;
  Item(this.id, this.title);
}

// ================================
// 模拟后端(FakeServer)
// ================================
class FakeServer {
  final List<_RawItem> _db = List.generate(
    53, (i) {

   return _RawItem(id: '${i + 1}', title: 'Item #${i + 1}');
  });

  List<_RawItem> getItemsByOffset(int offset, int limit) {
    if (offset >= _db.length) return [];
    final end = (offset + limit).clamp(0, _db.length);
    return _db.sublist(offset, end);
  }

  bool deleteItem(String id) {
    final index = _db.indexWhere((e) => e.id == id);
    if (index == -1) return false;
    _db.removeAt(index);
    return true;
  }

  int count() => _db.length;
}

class _RawItem {
  final String id;
  final String title;
  _RawItem({required this.id, required this.title});
}

🧠 总结优势

功能点是否支持说明
分页加载使用 offset 避免分页错乱
删除数据删除后自动更新 UI
删除补齐若不足 pageSize,则尝试补1条
删除整页自动回退上一页并刷新