1. 程式人生 > 實用技巧 >Flutter如何實現下拉重新整理和上拉載入更多

Flutter如何實現下拉重新整理和上拉載入更多

https://blog.csdn.net/huangxiaoguo1/article/details/85603172

  • 效果

  • 下拉重新整理

如果實現下拉重新整理,必須藉助RefreshIndicator,在listview外面包裹一層RefreshIndicator,然後在RefreshIndicator裡面實現onRefresh方法。

body: movieList.length == 0
          ? new Center(child: new CircularProgressIndicator())
          : new RefreshIndicator(
              color: const Color(0xFF4483f6),
              //下拉重新整理
              child: ListView.builder(
                itemCount: movieList.length + 1,
                itemBuilder: (context, index) {
                  if (index == movieList.length) {
                    return _buildProgressMoreIndicator();
                  } else {
                    return renderRow(index, context);
                  }
                },
                controller: _controller, //指明控制器載入更多使用
              ),
              onRefresh: _pullToRefresh,
            ),

onRefresh方法的實現_pullToRefresh,注意這裡必須使用async 不然報錯

 /**
   * 下拉重新整理,必須非同步async不然會報錯
   */
  Future _pullToRefresh() async {
    currentPage = 0;
    movieList.clear();
    loadMoreData();
    return null;
  }


非同步載入資料,注意:在Flutter中重新整理資料使用的是setState,不然無效,資料不會重新整理;資料的獲取需要使用[]取值,不能使用物件“ . ”的取值方法!

  //載入列表資料
  loadMoreData() async {
    this.currentPage++;
    var start = (currentPage - 1) * pageSize;
    var url =
        "https://api.douban.com/v2/movie/$movieType?start=$start&count=$pageSize";
    Dio dio = new Dio();
    Response response = await dio.get(url);
    setState(() {
      movieList.addAll(response.data["subjects"]);
      totalSize = response.data["total"];
    });
  }


上拉載入更多

載入更多需要對ListView進行監聽,所以需要進行監聽器的設定,在State中進行監聽器的初始化。

//初始化滾動監聽器,載入更多使用
  ScrollController _controller = new ScrollController();



在構造器中設定監聽
 //固定寫法,初始化滾動監聽器,載入更多使用
    _controller.addListener(() {
      var maxScroll = _controller.position.maxScrollExtent;
      var pixel = _controller.position.pixels;
      if (maxScroll == pixel && movieList.length < totalSize) {
        setState(() {
          loadMoreText = "正在載入中...";
          loadMoreTextStyle =
              new TextStyle(color: const Color(0xFF4483f6), fontSize: 14.0);
        });
        loadMoreData();
      } else {
        setState(() {
          loadMoreText = "沒有更多資料";
          loadMoreTextStyle =
              new TextStyle(color: const Color(0xFF999999), fontSize: 14.0);
        });
      }
    });


在listView中新增監聽controller方法

自此,Flutter如何實現下拉重新整理和上拉載入更多完成…

  • 整個列表頁面程式碼參考如下:
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:douban/pages/movie/movieDetail.dart';

class MovieList extends StatefulWidget {
  String movieType;
  //構造器傳遞資料(並且接收上個頁面傳遞的資料)
  MovieList({Key key, this.movieType}) : super(key: key);
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return new MovieListState(movieType: this.movieType);
  }
}

class MovieListState extends State<MovieList> {
  String movieType;
  String typeName;
  List movieList = new List();
  int currentPage = 0; //第一頁
  int pageSize = 10; //頁容量
  int totalSize = 0; //總條數
  String loadMoreText = "沒有更多資料";
  TextStyle loadMoreTextStyle =
      new TextStyle(color: const Color(0xFF999999), fontSize: 14.0);
  TextStyle titleStyle =
      new TextStyle(color: const Color(0xFF757575), fontSize: 14.0);
  //初始化滾動監聽器,載入更多使用
  ScrollController _controller = new ScrollController();

  /**
   * 構造器接收(MovieList)資料
   */
  MovieListState({Key key, this.movieType}) {
    //固定寫法,初始化滾動監聽器,載入更多使用
    _controller.addListener(() {
      var maxScroll = _controller.position.maxScrollExtent;
      var pixel = _controller.position.pixels;
      if (maxScroll == pixel && movieList.length < totalSize) {
        setState(() {
          loadMoreText = "正在載入中...";
          loadMoreTextStyle =
              new TextStyle(color: const Color(0xFF4483f6), fontSize: 14.0);
        });
        loadMoreData();
      } else {
        setState(() {
          loadMoreText = "沒有更多資料";
          loadMoreTextStyle =
              new TextStyle(color: const Color(0xFF999999), fontSize: 14.0);
        });
      }
    });
  }

  //載入列表資料
  loadMoreData() async {
    this.currentPage++;
    var start = (currentPage - 1) * pageSize;
    var url =
        "https://api.douban.com/v2/movie/$movieType?start=$start&count=$pageSize";
    Dio dio = new Dio();
    Response response = await dio.get(url);
    setState(() {
      movieList.addAll(response.data["subjects"]);
      totalSize = response.data["total"];
    });
  }

  @override
  void initState() {
    super.initState();
    //設定當前導航欄的標題
    switch (movieType) {
      case "in_theaters":
        typeName = "正在熱映";
        break;
      case "coming_soon":
        typeName = "即將上映";
        break;
      case "top250":
        typeName = "Top250";
        break;
    }

    //載入第一頁資料
    loadMoreData();
  }

  /**
   * 下拉重新整理,必須非同步async不然會報錯
   */
  Future _pullToRefresh() async {
    currentPage = 0;
    movieList.clear();
    loadMoreData();
    return null;
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(
      backgroundColor: Colors.white,
      appBar: new AppBar(
        leading: new IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed:null ,
        ),
        title: new Text(typeName != null ? typeName : "正在載入中...",
            style: new TextStyle(color: Colors.black)),
        backgroundColor: Colors.white,
      ),
      body: movieList.length == 0
          ? new Center(child: new CircularProgressIndicator())
          : new RefreshIndicator(
              color: const Color(0xFF4483f6),
              //下拉重新整理
              child: ListView.builder(
                itemCount: movieList.length + 1,
                itemBuilder: (context, index) {
                  if (index == movieList.length) {
                    return _buildProgressMoreIndicator();
                  } else {
                    return renderRow(index, context);
                  }
                },
                controller: _controller, //指明控制器載入更多使用
              ),
              onRefresh: _pullToRefresh,
            ),
    );
  }

  /**
   * 載入更多進度條
   */
  Widget _buildProgressMoreIndicator() {
    return new Padding(
      padding: const EdgeInsets.all(15.0),
      child: new Center(
        child: new Text(loadMoreText, style: loadMoreTextStyle),
      ),
    );
  }

  /**
   * 列表的ltem
   */
  renderRow(index, context) {
    var movie = movieList[index];
    var id = movie["id"];
    var title = movie["title"];
    var type = movie["genres"].join("、");
    var year = movie["year"];
    var score = movie["rating"]["average"];
    return new Container(
        height: 200,
        color: Colors.white,
        child: new InkWell(
          onTap: () {
            Navigator.of(context).push(new MaterialPageRoute(
                builder: (ctx) => new MovieDetail(movieId: id)));
          },
          child: new Column(
            children: <Widget>[
              new Container(
                height: 199,
                // color: Colors.blue,
                child: new Row(
                  children: <Widget>[
                    new Container(
                      width: 120.0,
                      height: 180.0,
                      margin: const EdgeInsets.all(10.0),
                      child: Image.network(movie["images"]["small"]),
                    ),
                    Expanded(
                      child: new Container(
                        height: 180.0,
                        margin: const EdgeInsets.all(12.0),
                        child: new Column(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            new Text(
                              "電影名稱:$title",
                              style: titleStyle,
                              overflow: TextOverflow.ellipsis,
                            ),
                            new Text(
                              "電影型別:$type",
                              style: titleStyle,
                              overflow: TextOverflow.ellipsis,
                            ),
                            new Text(
                              "上映年份:$year",
                              style: titleStyle,
                              overflow: TextOverflow.ellipsis,
                            ),
                            new Text(
                              "豆瓣評分:$score",
                              style: titleStyle,
                              overflow: TextOverflow.ellipsis,
                            )
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              //分割線
              new Divider(height: 1)
            ],
          ),
        ));
  }
}


///////

import 'dart:async';

import 'package:VPN/base/state.dart';

import 'package:VPN/gql/api/index.dart';

import 'package:VPN/style.dart';

import 'package:VPN/utils/toast.dart';

import 'package:VPN/widgets/index.dart';

import 'cupertino_refresh.dart';

typedef Future<List<T>> PageFuture<T>(int pageIndex);

typedef Future RefreshFuture();

typedef Widget ItemBuilder<T>(BuildContext context, T entry, int index);

class RefreshController<T> {

final List<T> _items;

final bool _usePrimary;

ScrollController _scrollController;

final _itemChangeNotifier = ChangeNotifier();

final _refreshNotifier = ChangeNotifier();

RefreshController({bool usePrimaryScrollController = true, Iterable<T> items})

: _items = items?.toList() ?? [],

_usePrimary = usePrimaryScrollController ?? true,

_scrollController =

(usePrimaryScrollController ?? true) ? null : ScrollController();

void refresh() {

_scrollController?.jumpTo(0);

// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member

_refreshNotifier.notifyListeners();

}

void updateItems(bool func(List<T> items)) {

final changed = func(_items);

if (changed) {

// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member

_itemChangeNotifier.notifyListeners();

}

}

void jumpToTop() {

_scrollController?.jumpTo(0.0);

}

void dispose() {

if (!_usePrimary) {

_scrollController?.dispose();

}

_itemChangeNotifier.dispose();

}

void _didChangeDependencies(BuildContext context) {

if (_usePrimary) {

_scrollController = PrimaryScrollController.of(context);

}

}

}

class Refresh<T> extends StatefulWidget {

/// 分頁列表

const Refresh.pagination({

Key key,

@required this.controller,

this.pageSize = API.PAGE_SIZE,

@required this.pageFuture,

@required this.itemBuilder,

this.separatorBuilder,

this.initialRefresh = true,

this.header,

this.refreshIndicatorBackgroundColor,

this.refreshIndicatorColor,

}) : assert(controller != null),

assert(pageSize != null),

assert(pageFuture != null),

assert(itemBuilder != null),

this.refreshFuture = null,

this.children = null,

this.gridDelegate = null,

super(key: key);

/// 固定列表

const Refresh.fixed({

Key key,

@required this.controller,

@required this.refreshFuture,

@required this.children,

this.initialRefresh = true,

this.header,

this.refreshIndicatorBackgroundColor,

this.refreshIndicatorColor,

}) : assert(controller != null),

assert(refreshFuture != null),

assert(children != null),

this.pageSize = null,

this.pageFuture = null,

this.itemBuilder = null,

this.gridDelegate = null,

this.separatorBuilder = null,

super(key: key);

/// 分頁表格

const Refresh.paginationGrid({

Key key,

@required this.controller,

this.pageSize = 20,

@required this.pageFuture,

@required this.itemBuilder,

@required this.gridDelegate,

this.initialRefresh = true,

this.header,

this.refreshIndicatorBackgroundColor,

this.refreshIndicatorColor,

}) : assert(controller != null),

assert(pageFuture != null),

assert(itemBuilder != null),

this.refreshFuture = null,

this.children = null,

this.separatorBuilder = null,

super(key: key);

final RefreshController<T> controller;

final int pageSize;

final PageFuture<T> pageFuture;

final ItemBuilder<T> itemBuilder;

// 固定列表

final RefreshFuture refreshFuture;

final List<Widget> children;

// 表格

final SliverGridDelegate gridDelegate;

final IndexedWidgetBuilder separatorBuilder;

/// 是否在初始化的時候觸發一次重新整理

final bool initialRefresh;

final Widget header;

final Color refreshIndicatorBackgroundColor;

final Color refreshIndicatorColor;

@override

_RefreshState<T> createState() => _RefreshState<T>();

}

class _RefreshState<T> extends BaseState<Refresh> {

VoidCallback _itemChangedListener;

VoidCallback _refreshListener;

int _pageIndex; // 從 1 開始

ValueNotifier<bool> _loadingMore;

bool _noMoreData;

bool _refreshing;

@override

void initState() {

super.initState();

_pageIndex = -1; // -1 表示第一次載入

_loadingMore = ValueNotifier<bool>(false);

_noMoreData = true;

_refreshing = false;

_itemChangedListener = () {

if (mounted) {

setState(() {});

}

};

widget.controller._itemChangeNotifier.addListener(_itemChangedListener);

_refreshListener = () async {

if (mounted) {

setState(() {

_refreshing = true;

});

}

// 觸發重新整理

await _onRefresh();

if (mounted) {

setState(() {

_refreshing = false;

});

}

};

widget.controller._refreshNotifier.addListener(_refreshListener);

}

@override

void didChangeDependencies() {

super.didChangeDependencies();

widget.controller._didChangeDependencies(context);

}

@override

void didUpdateWidget(Refresh oldWidget) {

super.didUpdateWidget(oldWidget);

if (widget.controller != oldWidget.controller) {

oldWidget.controller?._itemChangeNotifier

?.removeListener(_itemChangedListener);

oldWidget.controller?._refreshNotifier?.removeListener(_refreshListener);

widget.controller?._itemChangeNotifier?.addListener(_itemChangedListener);

widget.controller?._refreshNotifier?.addListener(_refreshListener);

}

}

@override

void dispose() {

widget.controller._refreshNotifier.removeListener(_refreshListener);

widget.controller._itemChangeNotifier.removeListener(_itemChangedListener);

super.dispose();

}

@override

Widget build(BuildContext context) {

final bool isPagination = widget.itemBuilder != null;

final slivers = <Widget>[

CupertinoSliverRefreshControl(

refreshTriggerPullDistance: 80.0,

refreshIndicatorExtent: 60.0,

onRefresh: _onRefresh,

initialRefresh: widget.initialRefresh,

builder: (context, refreshState, pulledExtent,

refreshTriggerPullDistance, refreshIndicatorExtent) {

return SimpleRefreshIndicator(

refreshState: refreshState,

pulledExtent: pulledExtent,

refreshTriggerPullDistance: refreshTriggerPullDistance,

refreshIndicatorExtent: refreshIndicatorExtent,

backgroundColor:

widget.refreshIndicatorBackgroundColor ?? AppColors.transparent,

color: widget.refreshIndicatorColor,

);

},

),

];

// 頭部

if (widget.header != null) {

slivers.add(SliverToBoxAdapter(child: widget.header));

}

if ((isPagination && widget.controller._items.isEmpty && _noMoreData) ||

(!isPagination && widget.children.length == 0)) {

// 沒有資料

if (_pageIndex >= 0) {

slivers.add(_emptySliver());

}

} else {

// 有資料

slivers.add(widget.gridDelegate == null ? _listSliver() : _gridSliver());

if (isPagination && !_noMoreData) {

// 還有更多資料的話

slivers.add(_loadMoreSliver());

}

}

Widget w;

w = CustomScrollView(

physics: const BouncingScrollPhysics(

parent: const AlwaysScrollableScrollPhysics(),

),

controller: widget.controller._scrollController,

slivers: slivers,

);

w = Stack(

alignment: Alignment.center,

children: <Widget>[

w,

...(!_refreshing

? []

: [

Container(

constraints: BoxConstraints.loose(Size(200, 200)),

padding: EdgeInsets.all(24),

decoration: const ShapeDecoration(

color: AppColors.popupBackground,

shape: const RoundedRectangleBorder(

borderRadius: const BorderRadius.all(

const Radius.circular(20),

),

),

),

child: LoadingIndicator(),

),

]),

],

);

return w;

}

Widget _listSliver() {

ListView listView;

if (widget.children != null) {

listView = ListView(

children: widget.children,

);

} else if (widget.separatorBuilder == null) {

listView = ListView.builder(

itemCount: widget.controller._items.length,

itemBuilder: (BuildContext context, int index) {

return widget.itemBuilder(

context,

widget.controller._items[index],

index,

);

},

);

} else {

listView = ListView.separated(

itemCount: widget.controller._items.length,

itemBuilder: (BuildContext context, int index) {

return widget.itemBuilder(

context,

widget.controller._items[index],

index,

);

},

separatorBuilder: widget.separatorBuilder,

);

}

// 包裹成 Sliver

Widget w;

w = SliverList(

delegate: listView.childrenDelegate,

);

return w;

}

Widget _gridSliver() {

GridView gridView;

gridView = GridView.builder(

itemCount: widget.controller._items.length,

gridDelegate: widget.gridDelegate,

itemBuilder: (BuildContext context, int index) {

return widget.itemBuilder(

context,

widget.controller._items[index],

index,

);

},

);

// 包裹成 Sliver

Widget w;

w = SliverGrid(

delegate: gridView.childrenDelegate,

gridDelegate: gridView.gridDelegate,

);

return w;

}

Widget _emptySliver() {

Widget w;

w = Row(

mainAxisAlignment: MainAxisAlignment.center,

children: <Widget>[

Icon(

Icons.info_outline,

size: 14,

color: AppColors.white70,

),

Text(

' 沒有記錄',

style: TextStyle(

color: AppColors.white70,

fontSize: 14,

),

),

],

);

w = Container(

width: double.infinity,

height: 60,

alignment: Alignment.center,

child: w,

);

w = IgnorePointer(child: w);

// 包裹成 Sliver

w = SliverToBoxAdapter(child: w);

return w;

}

Widget _loadMoreSliver() {

Widget w;

w = LoadMoreButton(

_loadingMore,

onTap: () {

_onLoadMore();

},

);

w = Container(

width: double.infinity,

height: 60,

alignment: Alignment.center,

child: w,

);

// 包裹成 Sliver

w = SliverToBoxAdapter(child: w);

return w;

}

Future _onRefresh() async {

final bool isPagination = widget.itemBuilder != null;

try {

await (isPagination ? _onLoadFirstPage : widget.refreshFuture)();

// 等待動畫結束再 refresh

while (isInTransition.value) {

await Future.delayed(Duration(milliseconds: 100));

}

} catch (e) {

if (mounted) {

showToast(e.toString());

}

}

}

/// 載入首頁資料

Future _onLoadFirstPage() async {

// 重置狀態

_pageIndex = 1;

_noMoreData = false;

final List<T> page = await widget.pageFuture(_pageIndex++);

widget.controller._items.clear();

if (page.length < widget.pageSize) {

_noMoreData = true;

}

widget.controller._items.addAll(page);

if (mounted) {

setState(() {});

}

}

/// 載入下一頁資料

Future _onLoadMore() async {

_loadingMore.value = true;

List<T> page;

try {

page = await widget.pageFuture(_pageIndex++);

} catch (_) {

_loadingMore.value = false;

return;

}

if (page.length < widget.pageSize) {

_noMoreData = true;

}

widget.controller._items.addAll(page);

_loadingMore.value = false;

if (mounted) {

setState(() {});

}

}

}

class LoadMoreButton extends StatefulWidget {

final ValueNotifier<bool> loadingMore;

final VoidCallback onTap;

const LoadMoreButton(this.loadingMore, {Key key, this.onTap})

: super(key: key);

@override

_LoadMoreButtonState createState() => _LoadMoreButtonState();

}

class _LoadMoreButtonState extends State<LoadMoreButton> {

VoidCallback _onStateChanged;

@override

void initState() {

super.initState();

_onStateChanged = () {

if (mounted) {

setState(() {});

}

};

widget.loadingMore?.addListener(_onStateChanged);

}

@override

void didUpdateWidget(LoadMoreButton oldWidget) {

super.didUpdateWidget(oldWidget);

if (widget.loadingMore != oldWidget.loadingMore) {

oldWidget.loadingMore?.removeListener(_onStateChanged);

widget.loadingMore?.addListener(_onStateChanged);

}

}

@override

void dispose() {

widget.loadingMore?.removeListener(_onStateChanged);

super.dispose();

}

@override

Widget build(BuildContext context) {

Widget w;

w = Row(

mainAxisSize: MainAxisSize.min,

mainAxisAlignment: MainAxisAlignment.center,

children: widget.loadingMore?.value != true

? <Widget>[

Icon(

Icons.all_inclusive,

size: 14,

color: AppColors.white,

),

Text(

' 點選載入更多',

style: TextStyle(

color: AppColors.white,

fontSize: 14,

),

),

]

: [

LoadingIndicator(

size: 14,

beginColor: AppColors.white,

endColor: AppColors.white,

),

],

);

w = Button(

child: w,

onPressed: () {

widget.onTap?.call();

},

);

return w;

}

}