前言

实现一个Flutter 应用中常见且核心的功能:上拉加载更多和下拉刷新。将从最基础的实现方式入手,使用 Flutter 内置的组件和控制器来构建这个功能。

核心思路

  1. 下拉刷新:使用 Flutter 官方提供的 RefreshIndicator 组件。它能够监听子组件的下拉手势,并在触发时执行一个回调函数,在这个回调中加载最新的数据。

  2. 上拉加载:通过监听 ListView 的 ScrollController 来实现。当用户滚动到列表底部附近时,判断是否需要加载下一页数据,并执行相应的数据请求和状态更新。

完整代码

import 'package:flutter/material.dart';

// 对联数据模型
class CoupletModel {
  final String upper; // 上联
  final String lower; // 下联
  final String horizontal; // 横批

  CoupletModel({
    required this.upper,
    required this.lower,
    required this.horizontal,
  });
}

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

  @override
  State<CoupletPage> createState() => _CoupletPageState();
}

class _CoupletPageState extends State<CoupletPage> {
  final List<CoupletModel> _dataList = []; // 数据源
  final ScrollController _scrollController = ScrollController(); // 滚动控制器

  int _page = 1; // 当前页码
  bool _isLoading = false; // 是否正在加载
  bool _hasMore = true; // 是否还有更多数据

  @override
  void initState() {
    super.initState();
    _refreshData(); // 初始化加载

    // 监听滚动实现上拉加载
    _scrollController.addListener(() {
      // 当滚动到距离底部小于 100 像素时,触发加载更多
      if (_scrollController.position.pixels >=
          _scrollController.position.maxScrollExtent - 100) {
        _loadMoreData();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  // 模拟网络请求方法
  Future<List<CoupletModel>> _fetchMockData(int page) async {
    await Future.delayed(const Duration(seconds: 30)); // 模拟网络延迟

    // 模拟第 4 页之后没有更多数据了
    if (page > 3) return [];

    return List.generate(10, (index) {
      int id = (page - 1) * 10 + index + 1;
      return CoupletModel(
        upper: "上联:岁岁平安节节高",
        lower: "下联:年年如意步步升",
        horizontal: "横批:大吉大利",
      );
    });
  }

  // 下拉刷新逻辑
  Future<void> _refreshData() async {
    setState(() {
      _page = 1;
      _hasMore = true;
    });

    List<CoupletModel> newData = await _fetchMockData(_page);

    setState(() {
      _dataList.clear();
      _dataList.addAll(newData);
    });
  }

  // 加载更多逻辑
  Future<void> _loadMoreData() async {
    if (_isLoading || !_hasMore) return;

    setState(() {
      _isLoading = true;
    });

    int nextPage = _page + 1;
    List<CoupletModel> newData = await _fetchMockData(nextPage);

    setState(() {
      _isLoading = false;
      if (newData.isEmpty) {
        _hasMore = false;
      } else {
        _page = nextPage;
        _dataList.addAll(newData);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("春节对联"),
        backgroundColor: Colors.white,
        foregroundColor: Colors.black,
      ),
      body: RefreshIndicator(
        onRefresh: _refreshData,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _dataList.length + 1, // +1 是为了显示底部的加载状态
          itemBuilder: (context, index) {
            // 如果是最后一项,根据状态显示“加载中”或“没有更多”
            if (index == _dataList.length) {
              return _buildFooter();
            }

            final item = _dataList[index];
            return _buildCoupletItem(item);
          },
        ),
      ),
    );
  }

  // 单个对联卡片布局
  Widget _buildCoupletItem(CoupletModel item) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      elevation: 3,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Text(
              item.horizontal,
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 18,
                color: Colors.red,
              ),
            ),
            const SizedBox(height: 10),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [_verticalText(item.upper), _verticalText(item.lower)],
            ),
          ],
        ),
      ),
    );
  }

  // 竖排文字组件
  Widget _verticalText(String text) {
    return SizedBox(
      width: 30,
      child: Text(
        text.replaceAll(":", "\n"), // 把冒号转行
        style: const TextStyle(
          fontSize: 16,
          height: 1.5,
          fontWeight: FontWeight.w500,
        ),
        textAlign: TextAlign.center,
      ),
    );
  }

  // 底部加载提示
  Widget _buildFooter() {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 20),
      child: Center(
        child: _hasMore
            ? const CircularProgressIndicator(strokeWidth: 2)
            : const Text(
                "--- 我是有底线的 ---",
                style: TextStyle(color: Colors.grey),
              ),
      ),
    );
  }
}
代码实现

1. 整体页面布局 (Scaffold 和 AppBar)

这是页面的最外层框架,由build方法中的Scaffold widget构建。
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text("春节对联"),
      backgroundColor: Colors.white,
      foregroundColor: Colors.black,
    ),
    body: RefreshIndicator(
      // ... a child widget
    ),
  );
}
  • Scaffold: 提供了标准的移动应用布局结构,包括顶部的应用栏(AppBar)和页面的主体内容(body)。

  • AppBar: 这是顶部的导航栏。

    • title: const Text("春节对联"): 设置了导航栏的标题文字。

    • backgroundColor: Colors.white: 将背景色设为白色。

    • foregroundColor: Colors.black: 将AppBar中所有前景元素(包括标题文字和默认的返回按钮图标)的颜色设为黑色,以确保在白色背景下可见。

 2.下拉刷新容器 (RefreshIndicator)

页面的主体body被一个RefreshIndicator包裹,这是实现下拉刷新功能的关键。

body: RefreshIndicator(
  onRefresh: _refreshData,
  child: ListView.builder(
    // ...
  ),
),
  • RefreshIndicator: 这是一个内置的Widget,当它的子组件(child)被向下滑动到足够距离时,它会显示一个Material Design风格的刷新动画,并触发onRefresh回调。

  • onRefresh: _refreshData: 将UI手势与业务逻辑连接起来。当用户执行下拉刷新操作时,RefreshIndicator会自动调用之前定义好的_refreshData方法。

  • childRefreshIndicator的子组件必须是可滚动的,这里放置了ListView.builder

3. 动态列表 (ListView.builder)

这是页面的核心内容区域,负责高效地显示对联列表。

child: ListView.builder(
  controller: _scrollController,
  itemCount: _dataList.length + 1, // +1 是为了显示底部的加载状态
  itemBuilder: (context, index) {
    // 如果是最后一项,根据状态显示“加载中”或“没有更多”
    if (index == _dataList.length) {
      return _buildFooter();
    }
    final item = _dataList[index];
    return _buildCoupletItem(item);
  },
),
  • ListView.builder: 这是一个高性能的列表构建器,它只会在列表项即将进入屏幕时才创建(build)它们,非常适合长列表。

  • controller: _scrollController: 将创建的滚动控制器_scrollControllerListView关联。这样就能通过监听控制器来获知列表的滚动位置,从而实现上拉加载。

  • itemCount: _dataList.length + 1: 这是实现底部加载提示的一个巧妙技巧。列表的总长度被设置为“数据列表的长度 + 1”。这个多出来的“+1”项就是用来显示“加载中”或“没有更多了”的占位符。

  • itemBuilder: 这是一个函数,用于根据索引index构建每一项的UI。

    • if (index == _dataList.length): 这是一个关键判断。当itemBuilder构建到最后一项(即那个“+1”的项)时,index正好等于数据列表的长度。这时,不渲染普通的对联卡片,而是调用_buildFooter()方法来渲染底部的加载提示。

    • else: 对于其他所有项,正常地从_dataList中取出数据,并调用_buildCoupletItem(item)来渲染一个对联卡片。

4. 底部加载提示 (_buildFooter)

用于构建列表最底部的UI,根据加载状态显示不同的内容。

Widget _buildFooter() {
  return Padding(
    padding: const EdgeInsets.symmetric(vertical: 20),
    child: Center(
      child: _hasMore
          ? const CircularProgressIndicator(strokeWidth: 2)
          : const Text(
              "--- 我是有底线的 ---",
              style: TextStyle(color: Colors.grey),
            ),
    ),
  );
}
  • Padding & Center: 用于提供一些垂直间距并让内容居中显示。

  • _hasMore ? ... : ... (三元运算符): 这是动态UI的核心。

    • 如果_hasMoretrue(意味着还有更多数据可以加载),则显示一个CircularProgressIndicator(圆形的加载动画)。

    • 如果_hasMorefalse(意味着所有数据都已加载完毕),则显示一段文本"--- 我是有底线的 ---"

5. 单个对联卡片 (_buildCoupletItem)

这个方法负责构建列表中每一张独立的对联卡片。

Widget _buildCoupletItem(CoupletModel item) {
  return Card(
    // ...
    child: Padding(
      // ...
      child: Column(
        children: [
          Text(item.horizontal, /* ... styles ... */),
          const SizedBox(height: 10),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [_verticalText(item.upper), _verticalText(item.lower)],
          ),
        ],
      ),
    ),
  );
}
  • Card: 作为卡片的根容器,它提供了Material Design风格的圆角、阴影和背景。

  • Padding: 在Card内部添加一些边距,让内容不会紧贴卡片边缘。

  • Column: 负责将卡片内容垂直排列:横批在上方,对联在下方。

  • Text(item.horizontal, ...): 显示横批,并设置了加粗、红色、大号字体的样式。

  • SizedBox(height: 10): 在横批和对联之间创建一个固定高度的间隙。

  • Row: 负责将上下联水平排列。mainAxisAlignment: MainAxisAlignment.spaceAround让上下联在水平方向上均匀分布空间。

  • _verticalText(...)Row的子组件是两个调用_verticalText方法生成的Widget,分别用于显示上联和下联。

6. 竖排文字组件 (_verticalText)

这是一个非常巧妙的自定义Widget,用于实现文字的竖向排列效果。

Widget _verticalText(String text) {
  return SizedBox(
    width: 30,
    child: Text(
      text.replaceAll(":", "\\n"), // 把冒号转行
      style: const TextStyle(
        fontSize: 16,
        height: 1.5,
        fontWeight: FontWeight.w500,
      ),
      textAlign: TextAlign.center,
    ),
  );
}
  • SizedBox(width: 30, ...): 关键点之一。它强制限制了Text组件的宽度。

  • text.replaceAll(":", "\\n")实现竖排的核心技巧。它将文本中的冒号替换为换行符\n。由于SizedBox的宽度非常窄(只能容下一个字),Text组件在渲染时会自动换行。通过将字符间的“分隔符”变成换行符,就实现了每个字占一行的效果,从而形成了视觉上的竖排。

  • style:

    • height: 1.5: 设置行高,增加了每个字之间的垂直间距,使其更美观。

    • textAlign: TextAlign.center: 确保每个字都在其狭窄的空间内居中。

效果预览

总结

通过 RefreshIndicator 和 ScrollController,用 Flutter 的原生方式实现了下拉刷新和上拉加载功能。

欢迎加入开源鸿蒙跨平台社区
https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐