Govaadiyo
Govaadiyo

Reputation: 6082

Pagination with ListView in flutter

I'm new at flutter and I have been searching for proper result of pagination for 2 days.

I followed this code Flutter ListView lazy loading But didn't achieve what I want. Look at below code.

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

So, When I reach at bottom of the page my _scrollListener method get call and there I have set setState(().... method to reload widget. Issue is I load my actual position and It starts with top of the list. So where I'm going wrong? Actually I want like. https://github.com/istyle-inc/LoadMoreTableViewController/blob/master/screen.gif

Edited:

Final code: (Guide by @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();
    }
  }
}

Thats working but Is it right/good way? little bit confused because if I reach at end of the page and scroll up/down it seems little bit sticky while scrolling..

Upvotes: 28

Views: 69697

Answers (6)

Wahab Khan Jadon
Wahab Khan Jadon

Reputation: 1176

You can check by either using ScrollController with maxScrollExtent and offset or by calculating with the help of index...

let's see the first approach...

set ScrollController() inside the class

final _scrollController = ScrollController();

Set this ListView controller inside the ListView.builder method...

controller: _scrollController

add listener inside the init method...

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

void _onScroll() {
    print("list view scrolling");
}

now u can listen to listview scrolling inside _onscroll method ...

with the following code you can check how much you can scroll on this page with the help of _scrollController.position.maxScrollExtent and your current scrolling position with the help of offset ... you can also play with the last return currentScroll >= (maxScroll * 0.9) by changing the value of 0.9 to 0.8 or less if you want to fetch a little bit early before user hit the listview bottom ...

bool get _isBottom {
    if (!_scrollController.hasClients) return false;
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.offset;
    return currentScroll >= (maxScroll * 0.9);
  }

now you can use _isButtom inside your _onScroll() method...

void _onScroll() {
  if (_isBottom) FetchedMore());
   //set currentPage+1 before making API call
   //or set startIndex to listViewData.length
}

use context.read for fetching more content if you are using some state manager i.e bloc / riverpod ... otherwise you have to put some extra logic to prevent sending multiple API calls for same page...

You may be wondering which page to fetch. just add currentPage+1 every time inside the FetchMore method if you are using page base pagination or set this ListView index if you are using startIndex approach according to your API needs...

now let's see the second approach calculating with help in current Index

with index calculate if you need to fetch next page like follow ...

void getMoreData(int index) {
    const pageSize = 10;
    final currentPosition = index + 1;
    //check if you need to loadMore
    final loadMore = currentPosition % pageSize == 0 && index != 0;
    //check which page needs to catch data 
    final int currentPageToRequest = currentPosition ~/ 10;

    if (loadMore && currentPageToRequest > state.pageNo) {
      state.copyWith(pageNo: state.pageNo + 1); //update the current page no
      getDataWithPage();
    }
  }

call this method inside ListView inside itemBuilder and pass the index...

Upvotes: 0

Siddharth Sogani
Siddharth Sogani

Reputation: 349

Easiest way without need of any scrollcontroller:

int offset = 0;
int limit = 10;
int posts = [];

getPosts(){
//get more posts and add them to posts array then increment skip = skip + limit
}

ListView.builder(
  itemCount: posts.length,
  itemBuilder: (BuildContext context, int index) {
    if (index == posts.length - 1) {
      getPosts();
    }
    return Column(
          children: [
yourItemWidget(posts[index]),
            if (index == posts.length - 1)
              CupertinoActivityIndicator(),
            if (index == posts.length - 1)
              SizedBox(
                height: 100,
              )
          ],);
  },
),

Upvotes: 0

Sanhaji Omar
Sanhaji Omar

Reputation: 332

https://pub.dev/packages/pagination_view is the package I found the more intuitive to work with that needed the least changes from a page view and felt the more intuitive:

It's easy to move from a listview to a PaginationView, the class that holds the widget needs to be stateful or it won't work:

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

Upvotes: 3

Rahul Narang
Rahul Narang

Reputation: 1

Try using this package, https://pub.dev/packages/paginate_firestore this made my day. You just need to add 2 attributes and its done!

It provides output in documentSnapshot and I tried it on ListView(works good) GridView has not been released till now as per contributor.

Upvotes: 0

Lakhwinder Singh
Lakhwinder Singh

Reputation: 7209

Add this to your listener of controller

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

Upvotes: 14

Hussein Abdallah
Hussein Abdallah

Reputation: 1550

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

you can use this package, or you can look at the code and redo it to suit your needs

Upvotes: 0

Related Questions