flutter 如何做无限滚动与分页在扑

cczfrluj  于 2023-04-22  发布在  Flutter
关注(0)|答案(6)|浏览(207)

我是一个新手,想用REST API做分页,我的问题是如何添加无限滚动然后加载数据到下一页,如何加载到“https://MY_API_URL?page=2”,第3页等等?谁能帮我一下?非常感谢

5fjcxozz

5fjcxozz1#

编辑将sendPagesDataRequest改为以下应该可以

如果你给我json字符串是正确的

Future<PagesData> sendPagesDataRequest(int page) async {
    print('page ${page}');
    try {
      /*String url = Uri.encodeFull(
          'http://api.worldbank.org/v2/country?page=$page&format=json');*/
      String url = Uri.encodeFull("https://MY_API_URL?page=$page");
      http.Response response = await http.get(url);
      print('body ${response.body}');

      /*String responseString = '''
      {"current_page": 1, 
"data": [ 
    { "id": 1, "title": "Germa", "likes": 5, "image": "https://picsum.photos/250?image=8"}, 
    { "id": 2, "title": "Jepun", "likes": 3, "image": "https://picsum.photos/250?image=9"} 
    ], 
 "first_page_url": "https:/API_URL?page=1", 
 "from": 1, 
 "last_page": 30, 
 "last_page_url": "https:/API_URLpage=30", 
 "next_page_url": "https:/API_URL?page=2"
}
      ''';*/

      PagesData pagesData = pagesDataFromJson(response.body);
      return pagesData;
    } catch (e) {
      if (e is IOException) {
        /*return CountriesData.withError(
            'Please check your internet connection.');*/
      } else {
        print(e.toString());
        /*return CountriesData.withError('Something went wrong.');*/
      }
    }
  }

编辑
新sendPagesDataRequest的完整代码

import 'dart:async';
import 'dart:convert';
import 'dart:io';

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

import 'package:flutter_paginator/flutter_paginator.dart';
import 'package:flutter_paginator/enums.dart';
import 'package:cached_network_image/cached_network_image.dart';

// To parse this JSON data, do
//
//     final pagesData = pagesDataFromJson(jsonString);

import 'dart:convert';

PagesData pagesDataFromJson(String str) => PagesData.fromJson(json.decode(str));

String pagesDataToJson(PagesData data) => json.encode(data.toJson());

class PagesData {
  int currentPage;
  List<Datum> data;
  String firstPageUrl;
  int from;
  int lastPage;
  String lastPageUrl;
  String nextPageUrl;

  PagesData({
    this.currentPage,
    this.data,
    this.firstPageUrl,
    this.from,
    this.lastPage,
    this.lastPageUrl,
    this.nextPageUrl,
  });

  factory PagesData.fromJson(Map<String, dynamic> json) => PagesData(
    currentPage: json["current_page"],
    data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
    firstPageUrl: json["first_page_url"],
    from: json["from"],
    lastPage: json["last_page"],
    lastPageUrl: json["last_page_url"],
    nextPageUrl: json["next_page_url"],
  );

  Map<String, dynamic> toJson() => {
    "current_page": currentPage,
    "data": List<dynamic>.from(data.map((x) => x.toJson())),
    "first_page_url": firstPageUrl,
    "from": from,
    "last_page": lastPage,
    "last_page_url": lastPageUrl,
    "next_page_url": nextPageUrl,
  };
}

class Datum {
  int id;
  String title;
  int likes;
  String image;

  Datum({
    this.id,
    this.title,
    this.likes,
    this.image,
  });

  factory Datum.fromJson(Map<String, dynamic> json) => Datum(
    id: json["id"],
    title: json["title"],
    likes: json["likes"],
    image: json["image"],
  );

  Map<String, dynamic> toJson() => {
    "id": id,
    "title": title,
    "likes": likes,
    "image": image,
  };
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Paginator',
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return HomeState();
  }
}

class HomeState extends State<HomePage> {
  GlobalKey<PaginatorState> paginatorGlobalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Paginator'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.format_list_bulleted),
            onPressed: () {
              paginatorGlobalKey.currentState
                  .changeState(listType: ListType.LIST_VIEW);
            },
          ),
          IconButton(
            icon: Icon(Icons.grid_on),
            onPressed: () {
              paginatorGlobalKey.currentState.changeState(
                listType: ListType.GRID_VIEW,
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2),
              );
            },
          ),
          IconButton(
            icon: Icon(Icons.library_books),
            onPressed: () {
              paginatorGlobalKey.currentState
                  .changeState(listType: ListType.PAGE_VIEW);
            },
          ),
        ],
      ),
      body: Paginator.listView(
        key: paginatorGlobalKey,
        pageLoadFuture: sendPagesDataRequest,
        pageItemsGetter: listItemsGetterPages,
        listItemBuilder: listItemBuilder,
        loadingWidgetBuilder: loadingWidgetMaker,
        errorWidgetBuilder: errorWidgetMaker,
        emptyListWidgetBuilder: emptyListWidgetMaker,
        totalItemsGetter: totalPagesGetter,
        pageErrorChecker: pageErrorChecker,
        scrollPhysics: BouncingScrollPhysics(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          paginatorGlobalKey.currentState.changeState(
              pageLoadFuture: sendCountriesDataRequest, resetState: true);
        },
        child: Icon(Icons.refresh),
      ),
    );
  }

  Future<CountriesData> sendCountriesDataRequest(int page) async {
    print('page ${page}');
    try {
      String url = Uri.encodeFull(
          'http://api.worldbank.org/v2/country?page=$page&format=json');
      http.Response response = await http.get(url);
      print('body ${response.body}');
      return CountriesData.fromResponse(response);
    } catch (e) {
      if (e is IOException) {
        return CountriesData.withError(
            'Please check your internet connection.');
      } else {
        print(e.toString());
        return CountriesData.withError('Something went wrong.');
      }
    }
  }

  Future<PagesData> sendPagesDataRequest(int page) async {
    print('page ${page}');
    try {
      /*String url = Uri.encodeFull(
          'http://api.worldbank.org/v2/country?page=$page&format=json');*/
      String url = Uri.encodeFull("https://MY_API_URL?page=$page");
      http.Response response = await http.get(url);
      print('body ${response.body}');

      /*String responseString = '''
      {"current_page": 1, 
"data": [ 
    { "id": 1, "title": "Germa", "likes": 5, "image": "https://picsum.photos/250?image=8"}, 
    { "id": 2, "title": "Jepun", "likes": 3, "image": "https://picsum.photos/250?image=9"} 
    ], 
 "first_page_url": "https:/API_URL?page=1", 
 "from": 1, 
 "last_page": 30, 
 "last_page_url": "https:/API_URLpage=30", 
 "next_page_url": "https:/API_URL?page=2"
}
      ''';*/

      PagesData pagesData = pagesDataFromJson(response.body);
      return pagesData;
    } catch (e) {
      if (e is IOException) {
        /*return CountriesData.withError(
            'Please check your internet connection.');*/
      } else {
        print(e.toString());
        /*return CountriesData.withError('Something went wrong.');*/
      }
    }
  }

  List<dynamic> listItemsGetter(CountriesData countriesData) {
    List<String> list = [];
    countriesData.countries.forEach((value) {
      list.add(value['name']);
    });
    return list;
  }

  List<dynamic> listItemsGetterPages(PagesData pagesData) {
    List<Datum> list = [];
    pagesData.data.forEach((value) {
      list.add(value);
    });
    return list;
  }

  Widget listItemBuilder(dynamic item, int index) {
    return Container(
      decoration: BoxDecoration(
          color: Colors.blue[50]
      ),
      margin: const EdgeInsets.all(8),
      child: Column(
        children: <Widget>[
          new CachedNetworkImage(
            imageUrl: item.image,
            placeholder: (context, url) => new CircularProgressIndicator(),
            errorWidget: (context, url, error) => new Icon(Icons.error),
          ),
          ListTile(title: Text(item.title), subtitle: Text('Likes: ' + item.likes.toString()),),
        ],),
    );
  }

  Widget loadingWidgetMaker() {
    return Container(
      alignment: Alignment.center,
      height: 160.0,
      child: CircularProgressIndicator(),
    );
  }

  Widget errorWidgetMaker(PagesData countriesData, retryListener) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text("error"),
        ),
        FlatButton(
          onPressed: retryListener,
          child: Text('Retry'),
        )
      ],
    );
  }

  Widget emptyListWidgetMaker(PagesData countriesData) {
    return Center(
      child: Text('No countries in the list'),
    );
  }

  int totalPagesGetter(PagesData pagesData) {
    return pagesData.lastPage;
  }

  bool pageErrorChecker(PagesData pagesData) {
    //return countriesData.statusCode != 200;
    return false;
  }
}

class CountriesData {
  List<dynamic> countries;
  int statusCode;
  String errorMessage;
  int total;
  int nItems;

  CountriesData.fromResponse(http.Response response) {
    this.statusCode = response.statusCode;
    List jsonData = json.decode(response.body);
    countries = jsonData[1];
    total = jsonData[0]['total'];
    nItems = countries.length;
  }

  CountriesData.withError(String errorMessage) {
    this.errorMessage = errorMessage;
  }
}

编辑

你需要改变sendPagesDataRequest,我使用静态字符串
假设你的json字符串是这样的

{"current_page": 1, 
"data": [ 
    { "id": 1, "title": "Germa", "likes": 5, "image": "image url"}, 
    { "id": 2, "title": "Jepun", "likes": 3, "image": "image url"} 
    ], 
 "first_page_url": "https:/API_URL?page=1", 
 "from": 1, 
 "last_page": 30, 
 "last_page_url": "https:/API_URLpage=30", 
 "next_page_url": "https:/API_URL?page=2"
}

编辑工作demo

编辑完整代码

import 'dart:async';
import 'dart:convert';
import 'dart:io';

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

import 'package:flutter_paginator/flutter_paginator.dart';
import 'package:flutter_paginator/enums.dart';
import 'package:cached_network_image/cached_network_image.dart';

// To parse this JSON data, do
//
//     final pagesData = pagesDataFromJson(jsonString);

import 'dart:convert';

PagesData pagesDataFromJson(String str) => PagesData.fromJson(json.decode(str));

String pagesDataToJson(PagesData data) => json.encode(data.toJson());

class PagesData {
  int currentPage;
  List<Datum> data;
  String firstPageUrl;
  int from;
  int lastPage;
  String lastPageUrl;
  String nextPageUrl;

  PagesData({
    this.currentPage,
    this.data,
    this.firstPageUrl,
    this.from,
    this.lastPage,
    this.lastPageUrl,
    this.nextPageUrl,
  });

  factory PagesData.fromJson(Map<String, dynamic> json) => PagesData(
    currentPage: json["current_page"],
    data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
    firstPageUrl: json["first_page_url"],
    from: json["from"],
    lastPage: json["last_page"],
    lastPageUrl: json["last_page_url"],
    nextPageUrl: json["next_page_url"],
  );

  Map<String, dynamic> toJson() => {
    "current_page": currentPage,
    "data": List<dynamic>.from(data.map((x) => x.toJson())),
    "first_page_url": firstPageUrl,
    "from": from,
    "last_page": lastPage,
    "last_page_url": lastPageUrl,
    "next_page_url": nextPageUrl,
  };
}

class Datum {
  int id;
  String title;
  int likes;
  String image;

  Datum({
    this.id,
    this.title,
    this.likes,
    this.image,
  });

  factory Datum.fromJson(Map<String, dynamic> json) => Datum(
    id: json["id"],
    title: json["title"],
    likes: json["likes"],
    image: json["image"],
  );

  Map<String, dynamic> toJson() => {
    "id": id,
    "title": title,
    "likes": likes,
    "image": image,
  };
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Paginator',
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return HomeState();
  }
}

class HomeState extends State<HomePage> {
  GlobalKey<PaginatorState> paginatorGlobalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Paginator'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.format_list_bulleted),
            onPressed: () {
              paginatorGlobalKey.currentState
                  .changeState(listType: ListType.LIST_VIEW);
            },
          ),
          IconButton(
            icon: Icon(Icons.grid_on),
            onPressed: () {
              paginatorGlobalKey.currentState.changeState(
                listType: ListType.GRID_VIEW,
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2),
              );
            },
          ),
          IconButton(
            icon: Icon(Icons.library_books),
            onPressed: () {
              paginatorGlobalKey.currentState
                  .changeState(listType: ListType.PAGE_VIEW);
            },
          ),
        ],
      ),
      body: Paginator.listView(
        key: paginatorGlobalKey,
        pageLoadFuture: sendPagesDataRequest,
        pageItemsGetter: listItemsGetterPages,
        listItemBuilder: listItemBuilder,
        loadingWidgetBuilder: loadingWidgetMaker,
        errorWidgetBuilder: errorWidgetMaker,
        emptyListWidgetBuilder: emptyListWidgetMaker,
        totalItemsGetter: totalPagesGetter,
        pageErrorChecker: pageErrorChecker,
        scrollPhysics: BouncingScrollPhysics(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          paginatorGlobalKey.currentState.changeState(
              pageLoadFuture: sendCountriesDataRequest, resetState: true);
        },
        child: Icon(Icons.refresh),
      ),
    );
  }

  Future<CountriesData> sendCountriesDataRequest(int page) async {
    print('page ${page}');
    try {
      String url = Uri.encodeFull(
          'http://api.worldbank.org/v2/country?page=$page&format=json');
      http.Response response = await http.get(url);
      print('body ${response.body}');
      return CountriesData.fromResponse(response);
    } catch (e) {
      if (e is IOException) {
        return CountriesData.withError(
            'Please check your internet connection.');
      } else {
        print(e.toString());
        return CountriesData.withError('Something went wrong.');
      }
    }
  }

  Future<PagesData> sendPagesDataRequest(int page) async {
    print('page ${page}');
    try {
      String url = Uri.encodeFull(
          'http://api.worldbank.org/v2/country?page=$page&format=json');
      http.Response response = await http.get(url);
      print('body ${response.body}');
      String responseString = '''
      {"current_page": 1, 
"data": [ 
    { "id": 1, "title": "Germa", "likes": 5, "image": "https://picsum.photos/250?image=8"}, 
    { "id": 2, "title": "Jepun", "likes": 3, "image": "https://picsum.photos/250?image=9"} 
    ], 
 "first_page_url": "https:/API_URL?page=1", 
 "from": 1, 
 "last_page": 30, 
 "last_page_url": "https:/API_URLpage=30", 
 "next_page_url": "https:/API_URL?page=2"
}
      ''';

      PagesData pagesData = pagesDataFromJson(responseString);
      return pagesData;
    } catch (e) {
      if (e is IOException) {
        /*return CountriesData.withError(
            'Please check your internet connection.');*/
      } else {
        print(e.toString());
        /*return CountriesData.withError('Something went wrong.');*/
      }
    }
  }

  List<dynamic> listItemsGetter(CountriesData countriesData) {
    List<String> list = [];
    countriesData.countries.forEach((value) {
      list.add(value['name']);
    });
    return list;
  }

  List<dynamic> listItemsGetterPages(PagesData pagesData) {
    List<Datum> list = [];
    pagesData.data.forEach((value) {
      list.add(value);
    });
    return list;
  }

  Widget listItemBuilder(dynamic item, int index) {
    return Container(
      decoration: BoxDecoration(
          color: Colors.blue[50]
      ),
      margin: const EdgeInsets.all(8),
      child: Column(
        children: <Widget>[
          new CachedNetworkImage(
            imageUrl: item.image,
            placeholder: (context, url) => new CircularProgressIndicator(),
            errorWidget: (context, url, error) => new Icon(Icons.error),
          ),
          ListTile(title: Text(item.title), subtitle: Text('Likes: ' + item.likes.toString()),),
        ],),
    );
  }

  Widget loadingWidgetMaker() {
    return Container(
      alignment: Alignment.center,
      height: 160.0,
      child: CircularProgressIndicator(),
    );
  }

  Widget errorWidgetMaker(PagesData countriesData, retryListener) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text("error"),
        ),
        FlatButton(
          onPressed: retryListener,
          child: Text('Retry'),
        )
      ],
    );
  }

  Widget emptyListWidgetMaker(PagesData countriesData) {
    return Center(
      child: Text('No countries in the list'),
    );
  }

  int totalPagesGetter(PagesData pagesData) {
    return pagesData.lastPage;
  }

  bool pageErrorChecker(PagesData pagesData) {
    //return countriesData.statusCode != 200;
    return false;
  }
}

class CountriesData {
  List<dynamic> countries;
  int statusCode;
  String errorMessage;
  int total;
  int nItems;

  CountriesData.fromResponse(http.Response response) {
    this.statusCode = response.statusCode;
    List jsonData = json.decode(response.body);
    countries = jsonData[1];
    total = jsonData[0]['total'];
    nItems = countries.length;
  }

  CountriesData.withError(String errorMessage) {
    this.errorMessage = errorMessage;
  }
}

您可以使用包https://pub.dev/packages/flutter_paginator
它将自动调用您的RESTpage参数
在下面的演示中,我添加了打印消息,所以你可以看到它在向下滚动时自动调用rest with page
你可以复制粘贴下面运行完整的代码
代码片段

Future<CountriesData> sendCountriesDataRequest(int page) async {
    print('page ${page}');
    try {
      String url = Uri.encodeFull(
          'http://api.worldbank.org/v2/country?page=$page&format=json');
      http.Response response = await http.get(url);
      print('body ${response.body}');
      return CountriesData.fromResponse(response);
    } catch (e) {
      if (e is IOException) {
        return CountriesData.withError(
            'Please check your internet connection.');
      } else {
        print(e.toString());
        return CountriesData.withError('Something went wrong.');
      }
    }
  }

工作演示

完整演示代码

import 'dart:async';
    import 'dart:convert';
    import 'dart:io';

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

    import 'package:flutter_paginator/flutter_paginator.dart';
    import 'package:flutter_paginator/enums.dart';

    void main() => runApp(MyApp());

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Paginator',
          home: HomePage(),
        );
      }
    }

    class HomePage extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return HomeState();
      }
    }

    class HomeState extends State<HomePage> {
      GlobalKey<PaginatorState> paginatorGlobalKey = GlobalKey();

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Flutter Paginator'),
            actions: <Widget>[
              IconButton(
                icon: Icon(Icons.format_list_bulleted),
                onPressed: () {
                  paginatorGlobalKey.currentState
                      .changeState(listType: ListType.LIST_VIEW);
                },
              ),
              IconButton(
                icon: Icon(Icons.grid_on),
                onPressed: () {
                  paginatorGlobalKey.currentState.changeState(
                    listType: ListType.GRID_VIEW,
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                        crossAxisCount: 2),
                  );
                },
              ),
              IconButton(
                icon: Icon(Icons.library_books),
                onPressed: () {
                  paginatorGlobalKey.currentState
                      .changeState(listType: ListType.PAGE_VIEW);
                },
              ),
            ],
          ),
          body: Paginator.listView(
            key: paginatorGlobalKey,
            pageLoadFuture: sendCountriesDataRequest,
            pageItemsGetter: listItemsGetter,
            listItemBuilder: listItemBuilder,
            loadingWidgetBuilder: loadingWidgetMaker,
            errorWidgetBuilder: errorWidgetMaker,
            emptyListWidgetBuilder: emptyListWidgetMaker,
            totalItemsGetter: totalPagesGetter,
            pageErrorChecker: pageErrorChecker,
            scrollPhysics: BouncingScrollPhysics(),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              paginatorGlobalKey.currentState.changeState(
                  pageLoadFuture: sendCountriesDataRequest, resetState: true);
            },
            child: Icon(Icons.refresh),
          ),
        );
      }

      Future<CountriesData> sendCountriesDataRequest(int page) async {
        print('page ${page}');
        try {
          String url = Uri.encodeFull(
              'http://api.worldbank.org/v2/country?page=$page&format=json');
          http.Response response = await http.get(url);
          print('body ${response.body}');
          return CountriesData.fromResponse(response);
        } catch (e) {
          if (e is IOException) {
            return CountriesData.withError(
                'Please check your internet connection.');
          } else {
            print(e.toString());
            return CountriesData.withError('Something went wrong.');
          }
        }
      }

      List<dynamic> listItemsGetter(CountriesData countriesData) {
        List<String> list = [];
        countriesData.countries.forEach((value) {
          list.add(value['name']);
        });
        return list;
      }

      Widget listItemBuilder(value, int index) {
        return ListTile(
          leading: Text(index.toString()),
          title: Text(value),
        );
      }

      Widget loadingWidgetMaker() {
        return Container(
          alignment: Alignment.center,
          height: 160.0,
          child: CircularProgressIndicator(),
        );
      }

      Widget errorWidgetMaker(CountriesData countriesData, retryListener) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(countriesData.errorMessage),
            ),
            FlatButton(
              onPressed: retryListener,
              child: Text('Retry'),
            )
          ],
        );
      }

      Widget emptyListWidgetMaker(CountriesData countriesData) {
        return Center(
          child: Text('No countries in the list'),
        );
      }

      int totalPagesGetter(CountriesData countriesData) {
        return countriesData.total;
      }

      bool pageErrorChecker(CountriesData countriesData) {
        return countriesData.statusCode != 200;
      }
    }

    class CountriesData {
      List<dynamic> countries;
      int statusCode;
      String errorMessage;
      int total;
      int nItems;

      CountriesData.fromResponse(http.Response response) {
        this.statusCode = response.statusCode;
        List jsonData = json.decode(response.body);
        countries = jsonData[1];
        total = jsonData[0]['total'];
        nItems = countries.length;
      }

      CountriesData.withError(String errorMessage) {
        this.errorMessage = errorMessage;
      }
    }

输出

I/flutter (20369): page 1
I/flutter (20369): body [{"page":1,"pages":7,"per_page":"50","total":304},[{"id":"ABW","iso2Code":"AW","name":"Aruba","region":{"id":"LCN","iso2code":"ZJ","value":"Latin America & Caribbean "},"adminregion":{"id":"","iso2code":"","value":""},"incomeLevel":{"id":"HIC","iso2code":"XD","value":"High income"},"lendingType":{"id":"LNX","iso2code":"XX","value":"Not classified"},"capitalCity":"Oranjestad","longitude":"-70.0167","latitude":"12.5167"},{"id":"AFG","iso2Code":"AF","name":"Afghanistan","region":{"id":"SAS","iso2code":"8S","value":"South Asia"},"adminregion":{"id":"SAS","iso2code":"8S","value":"South Asia"},"incomeLevel":{"id":"LIC","iso2code":"XM","value":"Low income"},"lendingType":{"id":"IDX","iso2code":"XI","value":"IDA"},"capitalCity":"Kabul","longitude":"69.1761","latitude":"34.5228"},{"id":"AFR","iso2Code":"A9","name":"Africa","region":{"id":"NA","iso2code":"NA","value":"Aggregates"},"adminregion":{"id":"","iso2code":"","value":""},"incomeLevel":{"id":"NA","iso2code":"NA","value":"Aggregates"},"lendingType":{"id":""
I/flutter (20369): page 2
I/flutter (20369): body [{"page":2,"pages":7,"per_page":"50","total":304},[{"id":"CIV","iso2Code":"CI","name":"Cote d'Ivoire","region":{"id":"SSF","iso2code":"ZG","value":"Sub-Saharan Africa "},"adminregion":{"id":"SSA","iso2code":"ZF","value":"Sub-Saharan Africa (excluding high income)"},"incomeLevel":{"id":"LMC","iso2code":"XN","value":"Lower middle income"},"lendingType":{"id":"IDX","iso2code":"XI","value":"IDA"},"capitalCity":"Yamoussoukro","longitude":"-4.0305","latitude":"5.332"},{"id":"CLA","iso2Code":"C6","name":"Latin America and the Caribbean (IFC classification)","region":{"id":"NA","iso2code":"NA","value":"Aggregates"},"adminregion":{"id":"","iso2code":"","value":""},"incomeLevel":{"id":"NA","iso2code":"NA","value":"Aggregates"},"lendingType":{"id":"","iso2code":"","value":"Aggregates"},"capitalCity":"","longitude":"","latitude":""},{"id":"CME","iso2Code":"C7","name":"Middle East and North Africa (IFC classification)","region":{"id":"NA","iso2code":"NA","value":"Aggregates"},"adminregion":{"id":"","iso2code":"","va
qqrboqgw

qqrboqgw2#

无限滚动分页是一项坚韧的任务。
除了延迟获取新项目之外,您还希望让用户了解您的当前状态。例如,如果您正在加载第一个页面,您可能希望在屏幕中间显示进度指示器。但是,如果您正在加载后续页面,您可能希望在底部显示进度指示器。错误指示器也是如此。
如果来自服务器的列表为空或已完成,您还需要停止请求新页面,更不用说您可能希望为失败的请求添加“重试”按钮。
现在有一个名为Infinite Scroll Pagination的包可以为您处理所有事情,用法非常简单。为了展示这一点,我将使用来自@chunhunghan answer的相同国家列表示例:

class CountryListView extends StatefulWidget {
  @override
  _CountryListViewState createState() => _CountryListViewState();
}

class _CountryListViewState extends State<CountryListView> {
  static const _pageSize = 20;

  final PagingController<int, Country> _pagingController =
      PagingController(firstPageKey: 0);

  @override
  void initState() {
    _pagingController.addPageRequestListener((pageKey) {
      _fetchPage(pageKey);
    });
    super.initState();
  }

  void _fetchPage(int pageKey) {
    RemoteApi.getCountryList(pageKey, _pageSize).then((newItems) {
      final isLastPage = newItems.length < _pageSize;
      if (isLastPage) {
        _pagingController.appendLastPage(newItems);
      } else {
        final nextPageKey = pageKey + newItems.length;
        _pagingController.appendPage(newItems, nextPageKey);
      }
    }).catchError((error) {
      _pagingController.error = error;
    });
  }

  @override
  Widget build(BuildContext context) => PagedListView<int, Country>(
        pagingController: _pagingController,
        builderDelegate: PagedChildBuilderDelegate<Country>(
          itemBuilder: (context, item, index) => CountryListItem(
            country: item,
          ),
        ),
      );

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

在上面的代码中,我在开头列出的所有问题(以及其他问题)都得到了解决,如果需要,您可以自定义所有内容。

  • 信息披露:我是软件包的作者,所以如果您有任何疑问,请随时给我发消息。*
tzdcorbm

tzdcorbm3#

我已经创建了一个轻量级的无限加载列表与分页的例子。新的项目被要求,因为你到达列表的底部。用法如下:

import 'package:flutter/material.dart';

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InfiniteList(
      widgetBuilder: (item) {
        return Text(item);
      },
      loadMore: (lastLoaded) {
        if (lastLoaded == null) {
          //first load request
          return ["hello", "world"];
        } else {
          //subsequent load request(s)
          return [];
        }
      },
      onItemSelected: (item) {
        print(item);
      },
    );
  }
}

我们的想法是根据最后一次加载的项目lastLoaded而不是页码进行分页。这样做有助于确保在加载页面X之后,如果页面X+1的内容发生更改(即从数据库中添加或删除某些内容),您不会错过或重复任何内容。
如果你的API不支持,或者你不想要它,你可以为你的每个项目添加一个页码属性,然后做:

something.load(page: lastLoaded.pageNumber + 1);

InfiniteList的实现看起来像这样:

import 'package:flutter/material.dart';

extension on List {
  Object lastOrNull() {
    return this.isNotEmpty ? this.last : null;
  }
}

typedef ItemWidgetBuilder = Widget Function(Object item);
typedef FutureItemsCallback = Future<List<Object>> Function(Object lastLoadedItem);

typedef ItemCallback = void Function(Object item);

class InfiniteList extends StatefulWidget {
  final ItemWidgetBuilder widgetBuilder;
  final FutureItemsCallback loadMore;
  final ItemCallback onItemSelected;

  InfiniteList({Key key, @required this.widgetBuilder, @required this.loadMore, this.onItemSelected}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return InfiniteListState();
  }
}

class InfiniteListState extends State<InfiniteList> {
  List<Object> items = [];
  bool shouldTryToLoadMore = true;

  @override
  void initState() {
    super.initState();
    waitOnItems();
  }

  void waitOnItems() async {
    try {
      final items = await widget.loadMore(this.items.lastOrNull());
      this.shouldTryToLoadMore = items.isNotEmpty;
      setState(() {
        this.items.addAll(items);
      });
    } catch(error) {
      print(error);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (items.isEmpty) {
      return initiallyLoading();
    } else {
      //TODO: show progress bar at the bottom if loading more
      return list();
    }
  }

  Widget list() {
    return ListView.builder(
        itemCount: shouldTryToLoadMore ? null : items.length,
        itemBuilder: (context, index) {
          if (shouldTryToLoadMore && index == items.length - 1) {
            waitOnItems();
            return null;
          } else if (index >= items.length) {
            return null;
          } else if (widget.onItemSelected != null) {
            return InkWell(
              onTap: () => {
                widget.onItemSelected(items[index])
              },
              child: widget.widgetBuilder(items[index]),
            );
          } else {
            return widget.widgetBuilder(items[index]);
          }
        }
      );
  }

  Widget initiallyLoading() {
    return Center(
      child: CircularProgressIndicator(),
    );
  }
}

完整的要点在这里:https://gist.github.com/tombailey/988f788493cec9b95e7e9e007b8a7a0d

z9smfwbn

z9smfwbn4#

屏幕_电影_列表.dart

import 'package:flutter/material.dart';

class ScreenMovieListing extends StatefulWidget {
  const ScreenMovieListing({Key? key}) : super(key: key);

  @override
  State<ScreenMovieListing> createState() => _ScreenMovieListingState();
}

class _ScreenMovieListingState extends State<ScreenMovieListing> {
  final _scrollController = ScrollController();
  bool isLoading = false;
  bool isLastPage = false;
  int currentPage = 0;
  int totalPage = 0;
  int totalItems = 0;
  var moviesList = [];

  @override
  void initState() {
    _restAPICall(true);
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        if (!isLastPage) {
          _restAPICall(false);
        }
      }
    });
  }

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

  void _restAPICall(bool clearList) async {
    if (clearList) {
      currentPage = 1;
      moviesList.clear();
    }

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

    await RestAPIHelper.movieList(
      context: context,
      currentPage: currentPage.toString(),
    ).then((response) {
      setState(() {
        isLoading = false;
      });

      if (response.status!) {
        setState(() {
          currentPage = response.pagignation!.currentPage!;
          totalPage = response.pagignation!.totalPages!;
          totalItems = response.pagignation!.totalItems!;

          if (currentPage == totalPage) {
            isLastPage = true;
          } else {
            currentPage = currentPage + 1;
          }

          moviesList.addAll(response.moviesListFromAPIResponse!);

          if (_scrollController.position.extentAfter <= 0 && isLoading == false) {
            // This code will call the pagination API if more data is available and screen had covered content
            if (!isLastPage) {
              _restAPICall(false);
            }
          }
        });
      }
    }).onError((error, stackTrace) {
      setState(() {
        isLoading = false;
      });
    });
  }

  Widget _loadMoreIndicator() {
    return !isLastPage
        ? const Padding(
            padding: EdgeInsets.symmetric(vertical: 10),
            child: Center(
              child: CircularProgressIndicator(),
            ),
          )
        : const SizedBox(height: 0);
  }

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      controller: _scrollController,
      shrinkWrap: true,
      itemCount: moviesList.length + 1,
      separatorBuilder: (_ctx, _index) {
        return SizedBox(height: 20);
      },
      itemBuilder: (_ctx, _index) {
        if (_index == moviesList.length) {
          return _loadMoreIndicator();
        } else {
          return ListItemMovie(data: moviesList[_index]);
        }
      },
    );
  }
}

rest_API_helper.dart

class RestAPIHelper {
  static Future<MoviesListModel> movieList(
    {BuildContext? context, String? currentPage = ''}) async {
    MoviesListModel? data;

    var bodyData = {
      'user_id': '1',
      'current_page': currentPage,
      'item_per_page': '1',
    };

    Map<String, String>? headerData = {
      'token': '1234567890',
    };

    try {
      final response = await http.post('api_url', headers: headerData, body: bodyData);
      var decodedResult = jsonDecode(response.body);
      data = MoviesListModel.fromJson(decodedResult);
      return data;
    } on SocketException {
      return null;
    } catch (error) {
      return null;
    }

    return data!;
  }
}

API响应

{
  "status": true,
  "message": "Movies Listing",
  "pagignation": {
    "current_page": 1,
    "total_pages": 7,
    "total_items": 7
  },
  "data": [
    {
      "id": "1",
      "name": "Movie Name",
      "image": "",
      "genre": "",
      "language": "Hindi",
      "release_year": "2022"
    }
  ]
}

本例基于请求参数如current_page、item_per_page和响应参数如current_page、total_pages、total_items的API

dpiehjr4

dpiehjr45#

I use ScrollController in Pagination ::

ScrollController? scrollController;

inside initState initialize it and pass the scrollListener function::

scrollController = ScrollController()..addListener(_scrollListener);

here is _scrollListener ::
- when extentAfter becomes 0 which means reached the bottom of last item you 
 call API again but you should also check if last page reached so that you 
 do not call API again when you reach last page , it is hashed inside the 
 condition

void _scrollListener() {
    debugPrint("extentAfter ::::: " +
        scrollController!.position.extentAfter.toString());
    if (scrollController!.position.extentAfter <= 0 /*&&
        viewModel.pageNumber < viewModel.totalCount + 1*/) {
      if (!viewModel.lazyLoading) {
        viewModel.lazyLoading = true;  //show a loading indicator
        // call APi again and inside getNewProducts "${pageNumber++}"
        viewModel.getNewProducts(viewModel.catId ?? ""); 
      }
    }
  }

then you pass scrollController to the listView/GridView controller ::

controller: scrollController,
bwitn5fc

bwitn5fc6#

用户infinite_scroll_pagination的Flutter包。
此软件包包含ListView、SliverList和SliverGrid无限滚动选项,并带有刷新指示符。
ListView无限滚动的示例是here

相关问题