Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do infinite scroll with pagination in flutter

Tags:

flutter

I am new in flutter. I want to do the pagination with REST API. My question is how to add infinite scroll and then load the data to the next page. How can I load to "https://MY_API_URL?page=2", page 3 and so on? Anyone can help me? Thank you very much

like image 419
dipgirl Avatar asked Dec 08 '19 09:12

dipgirl


People also ask

Is infinite scroll lazy loading?

Infinite scroll uses lazy loading and executes its demand to load more data (products or content) at the bottom of the page, without requiring an action such as the click of a button. On-demand loading is used to optimize content delivery by reducing time consumption and memory usage.


3 Answers

Edit change sendPagesDataRequest to the following should work
if json string you gave me is correct

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.');*/
      }
    }
  }

Edit
full code with new 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;
  }
}

Edit
you need to change sendPagesDataRequest, I use static string
Assume your json string like this

{"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"
}

Edit working demo

enter image description here

Edit full code

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;
  }
}

You can use package https://pub.dev/packages/flutter_paginator
It will auto call your REST with page parameter
In the following demo, I add print message , so you can see it auto call rest with page when scroll down
You can copy paste run full code below

code snippet

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.');
      }
    }
  }

working demo

enter image description here

full demo code

    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;
      }
    }

Output

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
like image 139
chunhunghan Avatar answered Oct 07 '22 06:10

chunhunghan


Infinite Scrolling Pagination is a tough task.

Besides just fetching new items lazily, you want to keep the user posted on your current state. For example, if you're loading the first page, you might want to show a progress indicator in the middle of the screen. But, if you're loading a subsequent page, you probably want to show a progress indicator at the bottom. The same is true for error indicators.

You also need to stop requesting new pages if the list from the server is either empty or completed. Not even to mention that you probably want to add "retry" buttons for failed requests.

There's now a package called Infinite Scroll Pagination that can handle everything for you, and the usage is pretty simple. To showcase that, I'll use the same country list example from @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();
  }
}

In the code above, all of the issues I listed in the beginning (and others) are addressed, and you can customize everything if you need.

Disclosure: I'm the package author, so feel free to message me with any doubts you may have.

like image 41
Edson Bueno Avatar answered Oct 07 '22 04:10

Edson Bueno


I have created a lightweight example of an infinite loading list with pagination. New items are requested as you reach the bottom of the list. Usage looks like this:

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);
      },
    );
  }
}

The idea is to paginate based on the last loaded item, lastLoaded rather than a page number. Doing this helps to ensure you don't miss or duplicate anything if the contents of page X+1 changes after you already loaded page X (i.e. when something is added or removed from the database).

If your API doesn't support that, or you don't want it, you could add a page number attribute to each of your items and then do:

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

The implementation for InfiniteList looks like this:

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(),
    );
  }
}

A full gist is here: https://gist.github.com/tombailey/988f788493cec9b95e7e9e007b8a7a0d

like image 1
Tom Bailey Avatar answered Oct 07 '22 05:10

Tom Bailey