欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里插入图片描述

案例概述

本案例展示如何实现分页列表,支持上一页、下一页、跳转等功能。分页是处理大数据集的常见方案,通过将数据分成多个页面,可以减少内存占用、提高应用性能。在 Flutter 中,实现分页列表需要管理当前页码、每页项数、总页数等状态,并根据这些状态动态生成当前页的数据。

分页列表在电商、内容管理、数据管理等应用中广泛使用。良好的分页实现应该支持多种导航方式(如上一页、下一页、直接跳转)、动态调整每页项数、缓存已加载的页面数据等功能。此外,对于 PC 端应用,还需要考虑响应式设计、键盘导航等因素。

核心概念

1. 分页逻辑与数据计算

分页的核心是根据当前页码和每页项数,计算出当前页应该显示的数据范围。总页数的计算公式是 (总项数 / 每页项数).ceil(),这确保了即使最后一页的项数不足每页项数,也会被计为一页。当前页的数据范围可以通过起始索引和结束索引来确定,起始索引为 (当前页码 - 1) * 每页项数,结束索引为 起始索引 + 每页项数

2. 分页导航与用户交互

分页导航提供了多种方式让用户在页面之间切换。最基础的是上一页和下一页按钮,允许用户逐页浏览数据。此外,还可以提供页码显示(如"第 2 页,共 10 页")和直接跳转功能(如输入页码直接跳转)。良好的分页导航设计应该清晰直观,让用户能够快速了解当前位置和总体数据量。

3. 分页状态管理

分页需要维护多个状态变量:当前页码(从 1 开始或从 0 开始,取决于实现)、每页项数、总页数等。此外,还需要根据当前页码判断是否可以进行上一页或下一页操作,以便禁用相应的按钮。这些状态的正确管理是实现可靠分页的关键。

代码详解

1. 基础分页

static const int itemsPerPage = 10;
int _currentPage = 1;

int get _totalPages => (_allItems.length / itemsPerPage).ceil();

List<String> get _currentPageItems {
  final startIndex = (_currentPage - 1) * itemsPerPage;
  final endIndex = startIndex + itemsPerPage;
  return _allItems.sublist(
    startIndex,
    endIndex > _allItems.length ? _allItems.length : endIndex,
  );
}

2. 分页导航

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    ElevatedButton(
      onPressed: _currentPage > 1 ? () => setState(() => _currentPage--) : null,
      child: Text('上一页'),
    ),
    Text('第 $_currentPage / $_totalPages 页'),
    ElevatedButton(
      onPressed: _currentPage < _totalPages ? () => setState(() => _currentPage++) : null,
      child: Text('下一页'),
    ),
  ],
)

3. 页码输入跳转

TextFormField(
  decoration: InputDecoration(labelText: '跳转到第几页'),
  onFieldSubmitted: (value) {
    final page = int.tryParse(value);
    if (page != null && page > 0 && page <= _totalPages) {
      setState(() => _currentPage = page);
    }
  },
)

高级话题:分页的企业级应用

1. 动态每页项数

void _changePageSize(int newSize) {
  setState(() {
    _itemsPerPage = newSize;
    _currentPage = 0;
  });
}

int get _totalPages => (_data.length / _itemsPerPage).ceil();

List<Map> get _currentPageData {
  final start = _currentPage * _itemsPerPage;
  final end = (start + _itemsPerPage).clamp(0, _data.length);
  return _data.sublist(start, end);
}

Widget _buildPaginator() {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      IconButton(
        icon: Icon(Icons.chevron_left),
        onPressed: _currentPage > 0 ? () => setState(() => _currentPage--) : null,
      ),
      Text('第 ${_currentPage + 1} 页,共 $_totalPages 页'),
      IconButton(
        icon: Icon(Icons.chevron_right),
        onPressed: _currentPage < _totalPages - 1 ? () => setState(() => _currentPage++) : null,
      ),
      DropdownButton<int>(
        value: _itemsPerPage,
        items: [5, 10, 20, 50].map((size) {
          return DropdownMenuItem(value: size, child: Text('$size 项/页'));
        }).toList(),
        onChanged: (size) => _changePageSize(size!),
      ),
    ],
  );
}

2. 智能缓存与预加载

class _PageCache {
  final Map<int, List<Map>> _cache = {};
  final int _maxCacheSize = 5;
  
  List<Map>? get(int page) => _cache[page];
  
  void set(int page, List<Map> data) {
    if (_cache.length >= _maxCacheSize) {
      _cache.remove(_cache.keys.first);
    }
    _cache[page] = data;
  }
  
  void clear() => _cache.clear();
}

void _preloadPages() {
  // 预加载前后两页
  if (_currentPage > 0) {
    _loadPage(_currentPage - 1);
  }
  _loadPage(_currentPage);
  if (_currentPage < _totalPages - 1) {
    _loadPage(_currentPage + 1);
  }
}

Future<void> _loadPage(int page) async {
  if (_pageCache.get(page) != null) return;
  
  final start = page * _itemsPerPage;
  final end = (start + _itemsPerPage).clamp(0, _data.length);
  final pageData = _data.sublist(start, end);
  
  _pageCache.set(page, pageData);
}

3. 无限滚动与虚拟列表

void _loadMore() {
  if (_currentPage < _totalPages - 1) {
    setState(() => _currentPage++);
  } else if (_hasMoreData) {
    // 从服务器加载更多数据
    _fetchMoreData();
  }
}

Future<void> _fetchMoreData() async {
  setState(() => _isLoading = true);
  try {
    final newData = await _api.fetchData(offset: _data.length);
    setState(() {
      _data.addAll(newData);
      _isLoading = false;
    });
  } catch (e) {
    setState(() {
      _error = e.toString();
      _isLoading = false;
    });
  }
}

// 虚拟列表实现
ListView.builder(
  itemCount: _currentPageData.length,
  itemBuilder: (context, index) {
    return ListTile(title: Text(_currentPageData[index]['name']));
  },
)

4. 搜索、过滤与排序

List<Map> get _filteredData {
  var result = _data.where((item) {
    final matchesSearch = _searchQuery.isEmpty ||
        item['name'].toLowerCase().contains(_searchQuery.toLowerCase());
    
    final matchesFilter = _selectedFilter == null ||
        item['status'] == _selectedFilter;
    
    return matchesSearch && matchesFilter;
  }).toList();
  
  // 应用排序
  if (_sortBy != null) {
    result.sort((a, b) {
      final aVal = a[_sortBy];
      final bVal = b[_sortBy];
      return _sortAscending ? aVal.compareTo(bVal) : bVal.compareTo(aVal);
    });
  }
  
  return result;
}

int get _totalPages => (_filteredData.length / _itemsPerPage).ceil();

List<Map> get _currentPageData {
  final start = _currentPage * _itemsPerPage;
  final end = (start + _itemsPerPage).clamp(0, _filteredData.length);
  return _filteredData.sublist(start, end);
}

5. 响应式分页设计

Widget _buildResponsivePaginator() {
  final width = MediaQuery.of(context).size.width;
  final isWideScreen = width > 1200;
  final isTablet = width > 600 && width <= 1200;
  
  if (isWideScreen) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        _buildPageSizeSelector(),
        _buildPageNavigator(),
        _buildPageInfo(),
      ],
    );
  } else if (isTablet) {
    return Column(
      children: [
        _buildPageNavigator(),
        SizedBox(height: 8),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            _buildPageSizeSelector(),
            _buildPageInfo(),
          ],
        ),
      ],
    );
  } else {
    return Column(
      children: [
        _buildPageNavigator(),
        SizedBox(height: 8),
        _buildPageSizeSelector(),
        SizedBox(height: 8),
        _buildPageInfo(),
      ],
    );
  }
}

6. 键盘导航与快捷键

Focus(
  onKey: (node, event) {
    if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
      if (_currentPage < _totalPages - 1) {
        setState(() => _currentPage++);
      }
      return KeyEventResult.handled;
    }
    if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
      if (_currentPage > 0) {
        setState(() => _currentPage--);
      }
      return KeyEventResult.handled;
    }
    if (event.isKeyPressed(LogicalKeyboardKey.home)) {
      setState(() => _currentPage = 0);
      return KeyEventResult.handled;
    }
    if (event.isKeyPressed(LogicalKeyboardKey.end)) {
      setState(() => _currentPage = _totalPages - 1);
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  },
  child: ListView(...),
)

7. 无障碍支持与屏幕阅读器

8. 分页的加载状态

bool _isLoading = false;

Future<void> _loadPage(int page) async {
  setState(() => _isLoading = true);
  try {
    final data = await _fetchPageData(page);
    setState(() => _currentPageItems = data);
  } finally {
    setState(() => _isLoading = false);
  }
}

9. 分页的错误处理

if (_hasError) {
  return Center(
    child: Column(
      children: [
        Text('加载失败'),
        ElevatedButton(
          onPressed: () => _loadPage(_currentPage),
          child: Text('重试'),
        ),
      ],
    ),
  );
}

10. 分页的测试

test('分页计算', () {
  expect(_totalPages, 5);
  expect(_currentPageItems.length, 10);
  _currentPage = 5;
  expect(_currentPageItems.length, 10);
});

通过这些企业级技巧,你可以构建出功能完整的分页系统。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐