Flutter Future Builder With Pagination

As a mobile developer, I started my journey as an Android Developer and I really liked it. After that, I learned  IOS and get frustrated because I need to do the same thing twice to maintain the same features. So, I kind of like hating myself and always find ways to share more code among those apps.

Then I heard news about this new SDK by Google called Flutter. I said to myself, this is awesome Flutter is good, instead of not like Android and IOS. But it is an elegant and efficient framework that will let us a truly single codebase for IOS and Android.

So enough of this intro, let’s make our hand dirty and write some Flutter code.

App Intro

In this blog, we’re gonna make a very simple movie fetching app. We’re gonna hit an URL and download the JSON data and show in a GridView. To fetch movies we’re gonna use TheMovieDb API. 

App Components
  1. FutureBuilder: Widget that builds itself based on the latest AsyncSnapshots. It serves as a bridge between Futures and the widget’s UI.
  2. Stateless Widget: A stateless widget has no internal state to manage. e.g Text, IconButton, Icon are examples of a stateless widget.
  3. Stateful Widget: A stateful widget is dynamic. The user can interact with a stateful widget or it changes over the time.
  4. GridView Builder: To show a list of movies in grid view. Create a scrollable 2D array of widgets.
  5. Futures: Flutter uses future objects to represent Asynchronous operation. If any code block takes a long time and if, we did not run the code block as an asynchronous the app freeze. Asynchronous operations let your program run without getting blocked.
  6. Json Mapping: Json mapping helps you to a parse json response came from web service.
  7. Paging: We’re gonna make a network request every time for new movie pages when a user reaches at the end of GridView
Flutter App Setup

Let’s start with the main function where the Flutter app run. Below is the run function of the Flutter app.

void main() {  
  runApp(new MaterialApp(
      title: "Movie Seacher",
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(),
      home: new MoviePage(),
  ));
}

Now before to show how we create a MoviePage widget, I wanna show you the movie API response. Now let’s make the Movie model so that we can parse the movie’s response.

Movie Model
class Movie {
  Movie(
      {this.title,
      this.posterPath,
      this.id,
      this.overview,
      this.voteAverage,
      this.favored});

  final String title, posterPath, id, overview;
  final String voteAverage;
  bool favored;

  factory Movie.fromJson(Map value) {
    return Movie(
        title: value['title'],
        posterPath: value['poster_path'],
        id: value['id'].toString(),
        overview: value['overview'],
        voteAverage: value['vote_average'].toString(),
        favored: false);
  }
}

In movie API response, we have movie title, posterPath, overview, and vote average and many other fields. All of the remaining fields we can skip if you want to read you can add them as a parameter. The Movie.fromJson is the method which is called every time when we need to parse a single movie object.

So, we parse the single Movie object but in movie API response we have page_results, page, total_results, and results which is a list of movies. Now we need another class which is MovieList. 

MovieList Model
class MovieList {
  MovieList({
    this.page,
    this.totalResults,
    this.totalPages,
    this.movies,
  });

  final int page;
  final int totalResults;
  final int totalPages;
  final List<Movie> movies;

  MovieList.fromMap(Map<String, dynamic> value)
      : page = value['page'],
        totalResults = value['total_results'],
        totalPages = value['total_pages'],
        movies = new List<Movie>.from(
            value['results'].map((movie) => Movie.fromJson(movie)));
}

You see in this class we’re parsing the complete movie API response and converting the results into List of movies.

Now we all love abstractions right so we’re gonna create a MovieRespository abstract class. So, that user not gonna interact with our backend API.

MovieRepository
abstract class MovieRepository {
  Future<ListMovies> fetchMovies(int pageNumber);
}

To fetch movies from API we need to pass page number to read movies. We’re returning Future because of our FutureBuilder needs the future object.

Now it’s time to see how we can make an async network request and convert it into a Future<ListMovie>. Below is the class in which we’re fetching movies and parsing them into a Future<ListMovie>.

MovieProdRepository
import 'dart:async';
import 'package:http/http.dart' as http;
import 'movie_data.dart';
import 'dart:convert';
import 'package:flutter/foundation.dart';

const MOVIE_API_KEY = "e5c7041343c************8b720e80c7";    // Replace with your own API key
const BASE_URL = "https://api.themoviedb.org/3/movie/";

class MovieProdRepository implements MovieRepository {
  @override
  Future<ListMovie> fetchMovies(int pageNumber) async {
    http.Response response = await http.get(BASE_URL +
        "popular?api_key=" +
        MOVIE_API_KEY +
        "&page=" +
        pageNumber.toString());
    return compute(parseMovies, response.body);
  }
}

ListMovie parseMovies(String responseBody) {
  final Map moviesMap = JsonCodec().decode(responseBody);
  print(moviesMap);
  ListMovie movies = ListMovie.fromMap(moviesMap);
  if (movies == null) {
    throw new Exception("An error occurred : [ Status Code = ]");
  }
  return movies;
}

In MovieProdRepository class we’re fetching movies and parsing them into ListMovie class. After parsing the response we’re gonna simply return the ListMovie as a Future. 

Now everything is done from the backend point of view. Let’s see how we can use the MovieRepository class, call the async function and show the movies into a GridView

Below is the MoviePage widget class. The MoviePage widget is a Stateless widget.

MoviePage
class _MoviePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new FutureBuilder<ListMovie>(
        future: movieRepository.fetchMovies(1),
        builder: (context, snapshots) {
          if (snapshots.hasError)
            return Text("Error Occurred");
          switch (snapshots.connectionState) {
            case ConnectionState.waiting:
              return Center(child: CircularProgressIndicator());
            case ConnectionState.done:
              return MovieTile(movies: snapshots.data);
            default:
          }
        });
  }
}

In this MoviePage widget, we’re executing our network requests to fetch movies. You see in FutureBuilder widget we’re passing the future object to FutureBuilder class. We’re showing CircularProgressIndicator widget unless the movie’s response request completes and then we call our MovieTile widget.

Below is the MovieTile widget class. The MovieTile widget is a stateful widget.

MovieTile
class MovieTile extends StatefulWidget {
  final ListMovie movies;

  MovieTile({Key key, this.movies}) : super(key: key);

  @override
  State<StatefulWidget> createState() => MovieTileState();
}

class MovieTileState extends State<MovieTile> {
  MovieLoadMoreStatus loadMoreStatus = MovieLoadMoreStatus.STABLE;
  final ScrollController scrollController = new ScrollController();
  static const String IMAGE_BASE_URL = "http://image.tmdb.org/t/p/w185";
  List<Movie> movies;
  int currentPageNumber;
  CancelableOperation movieOperation;

  @override
  void initState() {
    movies = widget.movies.movies;
    currentPageNumber = widget.movies.page;
    super.initState();
  }

  @override
  void dispose() {
    scrollController.dispose();
    if(movieOperation != null) movieOperation.cancel();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: onNotification,
      child: new GridView.builder(
        padding: EdgeInsets.only(
          top: 5.0,
        ),   // EdgeInsets.only
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 0.85,
        ),  // SliverGridDelegateWithFixedCrossAxisCount
        controller: scrollController,
        itemCount: movies.length,
        physics: const AlwaysScrollableScrollPhysics(),
        itemBuilder: (_, index) {
          return MovieListTile(movie: movies[index]);
        },
      ),  // GridView.builder
    );  // NotificationListener
  }

This is the main important widget where we are creating GridView, listening to the widget tree with NotificationListenerAdding ScrollController so that we can make a network request every time with new page number when a user reaches at the end of GridView.

You see in NotificationListener class we’re passing the onNotification function. The onNotification is basically accepting a function with bool return type and ScrollNotification as an input type.

One more thing noticeable here, you see I’m creating a CancelableOperation at the top MovieTileState class. This operation helps us to cancel the async request if the user navigates to a different screen. Later, I’m setting the CancelableOperation object to our movie page network request.

Below is the onNotification function.

bool onNotification(ScrollNotification notification) {
  if (notification is ScrollUpdateNotification) {
    if (scrollController.position.maxScrollExtent > scrollController.offset &&
        scrollController.position.maxScrollExtent - scrollController.offset <=
            50) {
      if (loadMoreStatus != null &&
          loadMoreStatus == MovieLoadMoreStatus.STABLE) {
        loadMoreStatus = MovieLoadMoreStatus.LOADING;
        movieOperation = CancelableOperation.fromFuture(injector
            .movieRepository
            .fetchMovies(currentPageNumber + 1)
            .then((moviesObject) {
          currentPageNumber = moviesObject.page;
          loadMoreStatus = MovieLoadMoreStatus.STABLE;
          setState(() => movies.addAll(moviesObject.movies));
        }));
      }
    }
  }
  return true;
}

In here we first check if the notification is ScrollNotification then we check if the user at the end of the GridView and last we make a network request to fetch new movies with a new page number. The LoadMoreStatus is the enum. We using LoadMoreStatus enum because we do not want to execute many network requests same if the one request is already executing.

MovieLoadMoreStatus
enum MovieLoadMoreStatus { LOADING, STABLE }

You guys must have noticed that we’re calling MovieListTile widget from MovieTile widget class. The MovieListTile widget is the single movie GridView tile.

MovieListTile
const String IMAGE_BASE_URL = "http://image.tmdb.org/t/p/w185";

class MovieListTile extends StatelessWidget {
  MovieListTile({this.movie});
  final Movie movie;

  @override
  Widget build(BuildContext context) {
    return Card(
      shape: RoundedRectangleBorder(
        borderRadius: new BorderRadius.all(
          new Radius.circular(15.0),
        ),  // BorderRadius.all
      ),  // RoundedRectangleBorder
      color: Colors.white,
      elevation: 5.0,
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Image(
              image: NetworkImageWithRetry(IMAGE_BASE_URL + movie.posterPath,
                  scale: 0.85),  // NetworkImageWithRetry
              fit: BoxFit.fill),  // Image
          _MovieFavoredImage(movie: movie),
          Align(
            alignment: Alignment.bottomRight,
            child: Padding(
              padding: EdgeInsets.only(bottom: 5.0, right: 5.0),
              child: Text('Rating : ${movie.voteAverage}'),
            ),  // Padding
          )  // Align
        ],  //  <Widget>[]
      ),  // Stack 
    );   // Card
  }
}

This widget class is very simple in here we simply create a CardView with an image inside it. To fetch movies poster we need to make another async. To fetch movies we’re using an image loading library. This library automatically fetches the image and show in the image and if the image failed due to the internet failure it automatically retries again.

MovieFavoredImage
class _MovieFavoredImage extends StatefulWidget {
  final Movie movie;
  _MovieFavoredImage({@required this.movie});

  @override
  State<StatefulWidget> createState() => _MovieFavoredImageState();
}

class _MovieFavoredImageState extends State<_MovieFavoredImage> {
  Movie currentMovie;

  @override
  void initState() {
    currentMovie = widget.movie;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return new Container(
      child: new Align(
        alignment: Alignment.topRight,
        child: new IconButton(
            icon: Icon(
              currentMovie.favored ? Icons.star : Icons.star_border,
            ),  // Icon 
            onPressed: onFavoredImagePressed),  // IconButton
      ),  // Align
    );  // Container
  }

  onFavoredImagePressed() {
    setState(() => currentMovie.favored = !currentMovie.favored);
  }
}

This widget is for when the user presses on the star icon and add the movie into Favorites. We need to make this widget Stateful because the widget changes its state when a user taps on the star icon from outline to filled star.

We can make MovieFavoredImage widget inside MovieListTile widget but when we update the state of the widget it updates the whole widget instead of just star icon state. That’s why we make another widget for just to update the star icon state, not the whole movie widget state.

If you want to see the code of the above example see it on GitHub. I wrote the complete example with Dependency Injection, Abstractions and with the Modular approach.

That’s it guys this is my demonstration about Future Builder with Pagination in Flutter. I hope you guy’s have learned something from this post. If you’ve any queries please do comment below.

Thank you for being here and keep reading…

3 COMMENTS

  1. Thanks. Your GitHub repo worked for me. I am trying to understand your code. Can you please tell me what is the purpose of dependency injector? and why you have used mock data when we are getting data from API.

    • Dependency injector is basically to get the Instance of MovieRepository depending on which Flavor you want. If you pass the PROD Flavor then you get the MovieProdRepository and with that, you get the movies from the network. If you pass the MOCK Flavor then you get the MovieMockRepository and with that, you get the dummy movies. Now the UI did not need to know from where to fetch movies, it just needs to know how to show data. By adding this approach the UI is transparent… Now if you just want to test the UI, you don’t need to pass the PROD Flavor you can pass MOCK Flavor.

LEAVE A REPLY

Please enter your comment!
Please enter your name here