Web Development | Ios and Android App Development Company in Charlotte Nc
+1 704 215 4622
[email protected]

Flutter: Paginated Lazy Loading ListView

Introduction

Google celebrated Flutter day on June 25th, a day for encouraging developers to try out their UI toolkit. Flutter is one of the most widely used open source frameworks based on Dart for cross platform development using a single codebase. The highlight of this being that the applications are developed using native compiled and not a simple JS bridge or WebVIew as seen in many other cross platform development tools. When developing mobile applications, most commonly it is a major headache for most developers to render dynamic lists in mobile applications, as it may result in significant performance losses if not handled properly.

Flutter already has provisions to manage large lists efficiently in memory by using the ListView widget. A straightforward approach would be to fetch all the data required to render the list and let the ListView handle the rest so that the resource utilization is kept in check. The lists still might be large when fetched from an external database and therefore result in large initial load times and excessive bandwidth utilization. So how should we tackle such an issue?

ScrollController _scrollController = ScrollController();
void initState() {
super.initState();
this._fetchData();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_fetchData();
}
});
}

Enter Lazy Loading

Lazy loading has been around for almost all major web applications. Basically, we load a minimal set of data initially to an empty list and append the next set of data to the list when required. This can be easily implemented by identifying whether the user has reached the end of the list and then fetching the next set accordingly. To identify whether the user has scrolled to the end of the list, we can initialize a ScrollController _scrollController which can listen to the event when the user reaches the end of the list with comparing the current position _scrollController.position.pixels with that of maximum scroll extent  _scrollController.position.maxScrollExtent.

Do keep in mind that the ScrollController will work in the background even if the user navigates away from the page and hence there are possibilities of memory leak if they aren’t cleared after use.

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

Fetching data

To fetch data from an API the http package from dart can be used. 

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

Here we must add http as a dependency in the pubspec.yaml file as shown below.

dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
http: 0.12.1

The _fetchData() function will get the API response and push the results to the albums

List. Each time a successful fetch occurs, we set the state using the setState() function, which in turn re-renders the list to show the newly fetched items. The API used here is a placeholder made available by jsonplaceholder.typicode.com.

The setState() function is actually the same as that of React/React Native. It simply notifies the widget that there are some changes in the variable states and hence UI needs to be re-rendered.

 String apiUrl = "https://jsonplaceholder.typicode.com/albums/1/photos";
 int pageNo = 1;
 ScrollController _scrollController = ScrollController();
 bool isLoading = false;
 List albums = List();
 
 void _fetchData() async {
   if (!isLoading) {
     setState(() {
       isLoading = true;
     });
     try {
       final response = await http.get(apiUrl);
       if (response.statusCode == 200) {
         List albumList = List();
         var resultBody;
         pageNo =
             (pageNo > 100) ? 1 : pageNo++; // resetting and incrementing page
         apiUrl = "https://jsonplaceholder.typicode.com/albums/$pageNo/photos";
 
         resultBody = jsonDecode(response.body);
         for (int i = 0; i < resultBody.length; i++) {
           albumList.add(resultBody[i]);
         }
         setState(() {
           isLoading = false;
           albums.addAll(albumList);
         });
       } else {
         setState(() {
           isLoading = false;
         });
       }
     } catch (_) {
       setState(() {
         isLoading = false;
       });
     }
   }

Using ListView.builder()

ListView.builder() is a named constructor which can be used to generate dynamic content. Instead of returning a static widget containing all the list items, the Listview.builder() function will create the ListView items as specified in the itemBuilder function and inflate them whenever they are required to be displayed on the screen. Thus the  ListView.builder() effectively manages memory when the list gets large. Here the _buildProgressIndicator()is used to indicate that the app is making a network call. 

Widget _buildList() {
   return albums.length < 1
       ? Center(
           child: Container(
             child: Text(
               'No data',
               style: TextStyle(
                 fontSize: 20.0,
               ),
             ),
           ),
         )
       : ListView.builder(
           itemCount: albums.length + 1,
           itemBuilder: (BuildContext context, int index) {
             if (index == albums.length) {
               return _buildProgressIndicator();
             } else {
               return Padding(
                 padding: EdgeInsets.all(8.0),
                 child: Card(
                   child: ListTile(
                     leading: CircleAvatar(
                       backgroundImage:
                           NetworkImage(albums[index]['thumbnailUrl']),
                     ),
                     title: Text((albums[index]['title'])),
                     onTap: () {
                       print(albums[index]);
                     },
                   ),
                 ),
               );
             }
           },
           controller: _scrollController,
         );
 
 Widget _buildProgressIndicator() {
   return Padding(
     padding: const EdgeInsets.all(8.0),
     child: Center(
       child: Opacity(
         opacity: isLoading ? 1.0 : 00,
         child: CircularProgressIndicator(),
       ),
     ),
   );
 }

The final main.dart should look like this:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
 
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: MyHomePage(),
   );
 }
}
 
class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() => _MyHomePageState();
}
 
class _MyHomePageState extends State<MyHomePage> {
 String apiUrl = "https://jsonplaceholder.typicode.com/albums/1/photos";
 int pageNo = 1;
 ScrollController _scrollController = ScrollController();
 bool isLoading = false;
 List albums = List();
 void _fetchData() async {
   if (!isLoading) {
     setState(() { isLoading = true; });
     final response = await http.get(apiUrl);
     if (response.statusCode == 200) {
       List albumList = List();
       var resultBody;
       pageNo = (pageNo > 100) ? 1 : pageNo++; // resetting and incrementing page
       apiUrl = "https://jsonplaceholder.typicode.com/albums/$pageNo/photos";
       resultBody = jsonDecode(response.body);
       for (int i = 0; i < resultBody.length; i++) {
         albumList.add(resultBody[i]);
       }
       setState(() {
         isLoading = false;
         albums.addAll(albumList);
       });
     } else {
       setState(() { isLoading = false; });
     }
   }
 }
 
 @override
 void initState() {
   this._fetchData();
   super.initState();
   _scrollController.addListener(() {
     if (_scrollController.position.pixels ==
         _scrollController.position.maxScrollExtent) {
       _fetchData();
     }
   });
 }
 
 @override
 void dispose() {
   _scrollController.dispose();
   super.dispose();
 }
 
 Widget _buildProgressIndicator() {
   return Padding(
     padding: const EdgeInsets.all(8.0),
     child: Center(
       child: Opacity(
         opacity: isLoading ? 1.0 : 00,
         child: CircularProgressIndicator(),
       ),
     ),
   );
 }
 
 Widget _buildList() {
   return albums.length < 1
       ? Center(
           child: Container(
             child: Text(
               'No data',
               style: TextStyle(
                 fontSize: 20.0,
               ),
             ),
           ),
         )
       : ListView.builder(
           itemCount: albums.length + 1,
           itemBuilder: (BuildContext context, int index) {
             if (index == albums.length) {
               return _buildProgressIndicator();
             } else {
               return Padding(
                 padding: EdgeInsets.all(8.0),
                 child: Card(
                   child: ListTile(
                     leading: CircleAvatar(
                       backgroundImage:
                           NetworkImage(albums[index]['thumbnailUrl']),
                     ),
                     title: Text((albums[index]['title'])),
                     onTap: () {
                       print(albums[index]);
                     },
                   ),
                 ),
               );
             }
           },
           controller: _scrollController,
         );
 }
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text("Pagination"),
     ),
     body: Container(
       child: _buildList(),
     ),
     resizeToAvoidBottomPadding: false,
   );
 }
}
 
 

The output for a list with progress widget will be as shown below.

Conclusion

Flutter is an incredible framework with lots of options for customization. Understanding Flutter widgets properly will allow you to quickly prototype a beautiful user interface which greatly reduces time for development and allows developers to focus on more important aspects of the application. Most importantly, seasoned developers can adopt various approaches for managing complex states in a way that they would prefer rather than following a set standard.

In the application built, it can be observed that most of the complexity involved in building a paginated list is easier to handle in Flutter. The implementation shown is a general one and can be modified to use for other use cases or real time databases like Firebase. 

About author View all posts

Aakarsh Baiju

Leave a Reply

Your email address will not be published. Required fields are marked *