likes
comments
collection
share

Flutter下拉刷新和上拉加载

作者站长头像
站长
· 阅读数 6

在Flutter中,可以通过多种方式实现下拉刷新和上拉加载的功能。其中一个非常流行的做法是使用RefreshIndicator来实现下拉刷新,结合ScrollController和底部加载指示器来实现上拉加载更多功能。

实现下拉刷新

import 'package:flutter/material.dart';

class MyRefreshableList extends StatefulWidget {
  @override
  _MyRefreshableListState createState() => _MyRefreshableListState();
}

class _MyRefreshableListState extends State<MyRefreshableList> {
  final List<String> items = List.generate(20, (i) => 'Item ${i + 1}');

  Future<void> _onRefresh() async {
    await Future.delayed(Duration(seconds: 2)); // 模拟网络请求
    // 更新数据
    setState(() {
      items
          .addAll(List.generate(20, (i) => 'New item ${i + items.length + 1}'));
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(items[index]));
        },
      ),
    );
  }
}

在这个例子中,RefreshIndicator配合ListView.builder实现下拉刷新。_onRefresh函数模拟了一个网络请求,并在完成后更新数据。

实现上拉加载更多

class MyLoadMoreList extends StatefulWidget {
  @override
  _MyLoadMoreListState createState() => _MyLoadMoreListState();
}

class _MyLoadMoreListState extends State<MyLoadMoreList> {
  final List<String> items = List.generate(20, (i) => 'Item ${i + 1}');
  final ScrollController _scrollController = ScrollController();
  bool isLoadingMore = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      // 滑动到底部时触发加载更多
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        _loadMore();
      }
    });
  }

  Future<void> _loadMore() async {
    if (!isLoadingMore) {
      setState(() => isLoadingMore = true);
      // 模拟网络请求结束后加载更多数据
      await Future.delayed(Duration(seconds: 2)); // 模拟网络请求延迟
      setState(() {
        items.addAll(
            List.generate(10, (i) => 'New item ${items.length + i + 1}'));
        isLoadingMore = false;
      });
    }
  }

  @override
  void dispose() {
    _scrollController.dispose(); // 不要忘记在dispose方法中清理控制器
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: items.length + 1, // 添加一个进度指示器作为最后一项
      itemBuilder: (context, index) {
        if (index == items.length) {
// 最后一项作为进度指示器
          return Visibility(
            visible: isLoadingMore,
            child: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }
        return ListTile(title: Text(items[index]));
      },
    );
  }
}


在这个例子中,我们使用一个ScrollController来检测列表是否滚动到底部。一旦到达底部,_loadMore函数会被触发,模拟加载更多内容的过程。同时,为了防止多次触发加载更多操作,我们添加了一个isLoadingMore变量作为标记。

ListView.builderitemCount设置为items.length + 1,这样列表的最后一项就是一个CircularProgressIndicator,显示加载中的动画。用Visibility控制它的显示,仅在加载更多数据时可见。

将上述下拉刷新和上拉加载更多功能结合起来,你就可以实现一个完整的下拉刷新和上拉加载更多的列表。

建议

  • 始终要记得在实现上拉加载时,需要在加载过程中防止重复触发加载方法。

  • 在生产环境中,用实际的网络请求替代示例中的延迟函数(即Future.delayed()),实现真正的分页加载数据。

  • 为了提升用户体验,可以在数据加载完成后,稍微滚动列表(通过ScrollController),以避免CircularProgressIndicator直接覆盖在最后一个列表项上。

下面是一个完整的实现示例,结合下拉刷新和上拉加载更多功能:

import 'package:flutter/material.dart';

class PullToRefreshAndLoadMore extends StatefulWidget {
  @override
  _PullToRefreshAndLoadMoreState createState() =>
      _PullToRefreshAndLoadMoreState();
}

class _PullToRefreshAndLoadMoreState extends State<PullToRefreshAndLoadMore> {
  final List<String> _items = List.generate(20, (i) => 'Item ${i + 1}');
  final ScrollController _scrollController = ScrollController();
  bool _isLoadingMore = false;
  bool _hasMore = true; // 表示是否还有更多数据可加载

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  Future<void> _onRefresh() async {
    await Future.delayed(Duration(seconds: 2));
    setState(() {
      _items.clear();
      _items.addAll(List.generate(20, (i) => 'Refreshed item ${i + 1}'));
    });
  }

  void _onScroll() {
    // 检测是否滚动到底部
    if (_scrollController.position.pixels >=
            _scrollController.position.maxScrollExtent &&
        !_isLoadingMore &&
        _hasMore) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (_isLoadingMore) return; // 如果已经在加载,则不执行后续操作
    setState(() {
      _isLoadingMore = true;
    });

    await Future.delayed(Duration(seconds: 2));

    if (mounted) {
      setState(() {
        _items.addAll(
            List.generate(10, (i) => 'New item ${_items.length + i + 1}'));

        // 假设每次增加了10个数据,加载了5次后认为没有更多数据
        if (_items.length >= 70) {
          _hasMore = false;
        }

        _isLoadingMore = false;
      });
    }
  }

  @override
  void dispose() {
    _scrollController.removeListener(_onScroll); // 移除滚动监听
    _scrollController.dispose(); // 清理控制器资源
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pull to Refresh & Load More'),
      ),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _hasMore
              ? _items.length + 1
              : _items.length, // 如果还有更多数据,添加额外一项来显示加载指示器
          itemBuilder: (context, index) {
            if (index == _items.length && _hasMore) {
// 最后一项为加载进度指示器
              return Center(
                child: Padding(
                  padding: EdgeInsets.all(8.0),
                  child: CircularProgressIndicator(),
                ),
              );
            }
            return ListTile(title: Text(_items[index]));
          },
        ),
      ),
    );
  }
}

在这个完成的示例中,_loadMore方法在滚动至底部时被触发,并使用setState来管理状态变化。同时,检查布尔标志_isLoadingMore来防止重复加载操作,以及用_hasMore判断是否还有更多数据需要加载。注意使用mounted检查以确保不会在Widget树移除后调用setState

通过结合RefreshIndicatorListView.builder,你可以创建一个用户友好的列表,支持下拉刷新和上拉加载更多数据,从而模拟典型的移动应用中常见的列表行为。请根据实际的业务逻辑和数据来源,适当调整示例中的模拟延迟和加载逻辑。

如何使用代码控制RefreshIndicator刷新呢?

在Flutter中,RefreshIndicator通常与一个手势滚动控件(如ListViewScrollView等)一起使用,用于提供下拉刷新的UI效果。要使用代码控制RefreshIndicator的刷新动作,你需要Key来引用它。

以下是通过代码触发RefreshIndicator刷新的步骤:

  1. RefreshIndicator创建一个GlobalKey<RefreshIndicatorState>
  2. 使用这个key来创建RefreshIndicator
  3. 通过key.currentState!.show()来触发刷新。

要注意的是,必须要在布局构建完成后调用,因为在布局构建过程中,组件的状态可能还未初始化。可以在WidgetsBinding.instance!.addPostFrameCallback中调用,确保在布局构建后执行。

这是一个使用代码触发RefreshIndicator的完整例子:

import 'package:flutter/material.dart';

class MyRefreshableList extends StatefulWidget {
  @override
  _MyRefreshableListState createState() => _MyRefreshableListState();
}

class _MyRefreshableListState extends State<MyRefreshableList> {
  final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      GlobalKey<RefreshIndicatorState>();
  final List<String> items = List.generate(20, (i) => 'Item $i');

  Future<void> _onRefresh() async {
    await Future.delayed(Duration(seconds: 2)); // 模拟网络请求
    setState(() {
      // 更新数据...
    });
  }

  // 代码触发下拉刷新
  void _triggerRefresh() {
    WidgetsBinding.instance!.addPostFrameCallback(
      (_) => _refreshIndicatorKey.currentState!.show(),
    );
  }

  @override
  Widget build(BuildContext context) {
    // 手动触发下拉刷新
    _triggerRefresh();

    return Scaffold(
        appBar: AppBar(
          title: Text('Pull to Refresh Example'),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.refresh),
              onPressed: _triggerRefresh,
            ),
          ],
        ),
        body: RefreshIndicator(
          key: _refreshIndicatorKey,
          onRefresh: _onRefresh,
          child: ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              return ListTile(title: Text(items[index]));
            },
          ),
        ));
  }
}

在这个例子中,_refreshIndicatorKeyRefreshIndicator的全局键(GlobalKey<RefreshIndicatorState>)。使用这个键,_triggerRefresh方法能够在布局构建后(通过addPostFrameCallback回调)调用show方法,从而触发RefreshIndicator的刷新。

_onRefresh方法中,通过模拟异步操作(Future.delayed)来模拟一个刷新动作,这应该代表实际的数据加载逻辑。在刷新完成后,通过调用setState来更新UI。

请注意,在本例中,_triggerRefresh方法在build方法中被调用,每次UI局部重建时,都会触发一次下拉刷新。这只是为了说明如何从代码中触发刷新。通常,你会将_triggerRefresh方法放置在某个特定事件或用户交互的回调中。

例如,你可以在用户选择Tab或任何其他逻辑导航到页面时,或用户通过按钮点击或自定义下拉手势时调用_triggerRefresh。如果下拉刷新应该在页面首次加载时发生,请确保只触发一次,以避免在每次页面重建时不断刷新。