在颤动中使用 ListView 进行分页

Posted

技术标签:

【中文标题】在颤动中使用 ListView 进行分页【英文标题】:Pagination with ListView in flutter 【发布时间】:2019-05-15 12:58:54 【问题描述】:

我是 Flutter 的新手,我一直在寻找合适的分页结果 2 天。

我遵循了这个代码Flutter ListView lazy loading 但没有达到我想要的。看下面的代码。

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';

main() => runApp(InitialSetupPage());

class InitialSetupPage extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: "API Pagination",
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.green),
      home: HomePage(),
    );
  


class HomePage extends StatefulWidget 
  @override
  _HomePageState createState() => _HomePageState();


class _HomePageState extends State<HomePage> 
  int pageNum = 1;
  bool isPageLoading = false;
  List<Map<String, dynamic>> arrayOfProducts;

  Future<List<Map<String, dynamic>>> _callAPIToGetListOfData() async 

    isPageLoading = true;
    final responseDic =
        await http.post(urlStr, headers: accessToken, body: paramDic);
    Map<String, dynamic> dicOfRes = json.decode(responseDic.body);
    List<Map<String, dynamic>> temArr = List<Map<String, dynamic>>.from(dicOfRes["data"]["products"]);

    if (pageNum == 1) 
      arrayOfProducts = temArr;
     else 
      arrayOfProducts.addAll(temArr);
    

    return arrayOfProducts;
  

  ScrollController controller;

  @override
  void initState() 
    controller = new ScrollController()..addListener(_scrollListener);
    super.initState();
  

  _scrollListener() 
    print(controller.position.extentAfter);
    if (controller.position.extentAfter <= 0 && isPageLoading == false) 
      _callAPIToGetListOfData().then((data)
        setState(() 

        );
      );
    
  

  @override
  void dispose() 
    controller.removeListener(_scrollListener);
    super.dispose();
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
        appBar: AppBar(
          title: Text("Online Products"),
          centerTitle: true,
        ),
        body: Container(
          child: FutureBuilder<List<Map<String, dynamic>>>(
              future: _callAPIToGetListOfData(),
              builder: (BuildContext context, AsyncSnapshot snapshot) 
                switch (snapshot.connectionState) 
                  case ConnectionState.none:
                  case ConnectionState.active:
                  case ConnectionState.waiting:
                    return Center(child: CircularProgressIndicator());
                  case ConnectionState.done:
                    if (snapshot.hasError) 
                      Text(
                          'YOu have some error : $snapshot.hasError.toString()');
                     else if (snapshot.data != null) 
                      isPageLoading = false;
                      pageNum++;

                      print(arrayOfProducts);
                      return Scrollbar(
                        child: ListView.builder(
                            itemCount: arrayOfProducts.length,
                            controller: controller,
                            physics: AlwaysScrollableScrollPhysics(),
                            itemBuilder: (context, index) 
                              return ListTile(
                                title: Text(
                                    '$index $arrayOfProducts[index]['title']'),
                              );
                            ),
                      );
                    
                
              ),
        ));
  

所以,当我到达页面底部时,我的_scrollListener 方法得到调用,我已经设置setState(().... 方法来重新加载小部件。问题是我加载了我的实际位置,它从列表顶部开始。那么我哪里错了?其实我想要喜欢。 https://github.com/istyle-inc/LoadMoreTableViewController/blob/master/screen.gif

编辑:

最终代码: @Rémi Rousselet 指导)

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';

main() => runApp(InitialSetupPage());

class InitialSetupPage extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: "API Pagination",
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.green),
      home: HomePage(),
    );
  


class HomePage extends StatefulWidget 
  @override
  _HomePageState createState() => _HomePageState();


class _HomePageState extends State<HomePage> 
  int pageNum = 1;
  bool isPageLoading = false;
  List<Map<String, dynamic>> arrayOfProducts;
  ScrollController controller;
  Future<List<Map<String, dynamic>>> future;
  int totalRecord = 0;

  @override
  void initState() 
    controller = new ScrollController()..addListener(_scrollListener);
    future = _callAPIToGetListOfData();

    super.initState();
  

  @override
  void dispose() 
    controller.removeListener(_scrollListener);
    super.dispose();
  

  @override
  Widget build(BuildContext context) 
    final constListView = ListView.builder(
        itemCount: arrayOfProducts == null ? 0 : arrayOfProducts.length,
        controller: controller,
        physics: AlwaysScrollableScrollPhysics(),
        itemBuilder: (context, index) 
          return Column(
            children: <Widget>[
              ListTile(
                title: Text('$index $arrayOfProducts[index]['title']'),
                leading: CircleAvatar(backgroundImage: NetworkImage(arrayOfProducts[index]['thumbnail'] ?? "")),
              ),
              Container(
                color: Colors.black12,
                height: (index == arrayOfProducts.length-1 && totalRecord > arrayOfProducts.length) ? 50 : 0,
                width: MediaQuery.of(context).size.width,
                child:Center(
                  child: CircularProgressIndicator()
                ),
              )
            ],
          );
        );
    return Scaffold(
        appBar: AppBar(
          title: Text("Online Products"),
          centerTitle: true,
        ),
        body: Container(
          child: FutureBuilder<List<Map<String, dynamic>>>(
              future: future,
              builder: (BuildContext context, AsyncSnapshot snapshot) 
                switch (snapshot.connectionState) 
                  case ConnectionState.none:
                  case ConnectionState.active:
                  case ConnectionState.waiting:
                    return Center(child: CircularProgressIndicator());
                  case ConnectionState.done:
                    if (snapshot.hasError) 
                      Text(
                          'YOu have some error : $snapshot.hasError.toString()');
                     else if (snapshot.data != null) 
                      isPageLoading = false;

                      print(arrayOfProducts);
                      return constListView;
                    
                
              ),
        ));
  

  Future<List<Map<String, dynamic>>> _callAPIToGetListOfData() async 

    isPageLoading = true;
    final responseDic =
        await http.post(urlStr, headers: accessToken, body: paramDic);
    Map<String, dynamic> dicOfRes = json.decode(responseDic.body);
    List<Map<String, dynamic>> temArr =
        List<Map<String, dynamic>>.from(dicOfRes["data"]["products"]);
    setState(() 
      if (pageNum == 1) 
        totalRecord = dicOfRes["total_record"];
        print('============>>>>>>> $totalRecord');
        arrayOfProducts = temArr;
       else 
        arrayOfProducts.addAll(temArr);
      
      pageNum++;
    );
    return arrayOfProducts;
  

  _scrollListener() 
    if (totalRecord == arrayOfProducts.length) 
      return;
    
    print(controller.position.extentAfter);
    if (controller.position.extentAfter <= 0 && isPageLoading == false) 
      _callAPIToGetListOfData();
    
  

那行得通,但它是正确/好方法吗?有点困惑,因为如果我到达页面末尾并向上/向下滚动,滚动时似乎有点粘..

【问题讨论】:

为什么不使用itemBuilder 来延迟加载数据? 我已经用过请检查一下。 不,您正在使用ScrollController - 我的意思是从itemBuilder 启动按需数据加载 ***.com/questions/49508322/… 怎么没有做你想做的事? @RémiRousselet 每次新页面从 ListView 的顶部开始,而不是它应该停留在我们滚动的末尾。 【参考方案1】:

将此添加到您的控制器侦听器

if (scrollController.position.maxScrollExtent == scrollController.offset) 

        /***
         * we need to get new data on page scroll end but if the last
         * time when data is returned, its count should be Constants.itemsCount' (10)
         *
         * So we calculate every time
         *
         * productList.length >= (Constants.itemsCount*pageNumber)
         *
         * list must contain the products == Constants.itemsCount if the page number is 1
         * but if page number is increased then we need to calculate the total
         * number of products we have received till now example:
         * first time on page scroll if last count of productList is Constants.itemsCount
         * then increase the page number and get new data.
         * Now page number is 2, now we have to check the count of the productList
         * if it is==Constants.itemsCount*pageNumber (20 in current case) then we have
         * to get data again, if not then we assume, server has not more data then
         * we currently have.
         *
         */
        if (productList.length >= (Constants.itemsCount * pageNumber) &&
            !isLoading) 
          pageNumber++;
          print("PAGE NUMBER $pageNumber");
          print("getting data");
          getProducts(false); // Hit API to get new data
        
      

【讨论】:

Constants.itemsCount 怎么得到? 这只是一个计数,这样我每次都能得到 Constant.itemsCount 个项目 scrollController.position.maxScrollExtent == scrollController.offset 这一行节省了我的时间。 +尊重【参考方案2】:

https://pub.dev/packages/pagination_view 是我发现使用起来更直观的包,需要从页面视图中进行最少的更改并且感觉更直观:

从列表视图移动到分页视图很容易,保存小部件的类需要是有状态的,否则它将不起作用:

      PaginationView<User>(
        preloadedItems: <User>[
          User(faker.person.name(), faker.internet.email()),
          User(faker.person.name(), faker.internet.email()),
        ],
        itemBuilder: (BuildContext context, User user, int index) => ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
          leading: IconButton(
            icon: Icon(Icons.person),
            onPressed: () => null,
          ),
        ),
        paginationViewType: PaginationViewType.listView // optional
        pageFetch: pageFetch,
        onError: (dynamic error) => Center(
          child: Text('Some error occured'),
        ),
        onEmpty: Center(
          child: Text('Sorry! This is empty'),
        ),
        bottomLoader: Center( // optional
          child: CircularProgressIndicator(),
        ),
        initialLoader: Center( // optional
          child: CircularProgressIndicator(),
        ),
      ),

【讨论】:

【参考方案3】:

https://pub.dartlang.org/packages/loadmore

您可以使用这个包,也可以查看代码并重做以满足您的需求

【讨论】:

谢谢,但我不想使用包。为了学习,我想做自己。 github.com/OpenFlutter/flutter_listview_loadmore/blob/master/… 如前面的 cmets 所说,使用类似以下代码的 itembuilder。它比使用滚动控制器要简单得多!【参考方案4】:

尝试使用这个包,https://pub.dev/packages/paginate_firestore 这让我很快乐。您只需要添加 2 个属性即可!

它在 documentSnapshot 中提供输出,我在 ListView 上尝试过(效果很好)根据贡献者的说法,GridView 直到现在还没有发布。

【讨论】:

以上是关于在颤动中使用 ListView 进行分页的主要内容,如果未能解决你的问题,请参考以下文章

如何在颤动中使用listview builder制作动态单选按钮?

颤动中滚动视图内的Listview

一些滚动后颤动ListView KeepAlive

在listview中颤动Custompaint:忽略两个手指滚动

如何在颤动/飞镖的 ListView 项目中更改背景颜色

无法在颤动中过滤 listview.builder 的列表