Tag

Flutter

Browsing

If you’re coming from an Android background like me then you’ve probably missed those POJO classes in Flutter. I believe the developers who do app development in flutter will encounter such problems. After requesting data from the server, the server will often return a json string and if we want to use data flexibly, we need to convert the json string into an object .

Since flutter only provides json to Map. Handwritten deserialization is extremely unstable in large projects and can easily lead to parsing failure. So today I will introduce you to json_annotation, an automatic deserialization library recommended by flutter team.

What you’ll learn

  • Generate code with build_runner.
  • How to parse json object in the flutter with json_serialization.

Include Dependencies

We’re gonna need to add some libraries in the pupspec.yaml, which is the package management and build system file. Here we need to add three dependencies json_annotationbuild_runner and json_serializable in the pupspec.yamlfile.

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  json_annotation: ^1.2.0  // dependecy

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.0.0              //  |
                                    //  | -> dev dependencies
  json_serializable: ^1.5.1         //  |

Once you have done these run packets get which is in the toolbar of the file from IntelliJ/Android Studio you can also execute flutter packages pub get  from the terminal in the current project directory if you prefer that.

Now let’s say we need to make a login request and download some json content. The following shows the sample json of a simplified login rest call.

{
  "status" : true,
  "message" : "User successfuly logged in!",
  "user_name" : "Ahsen Saeed",
  "profile_url" : "www.codinginfinite.com",
  "user_id" : 280
}

Now we need to write a dart entity class based on above json data.

class LoginResponse{
  bool status;
  String message;
  String userName;
  String profileUrl;
  int userId;
  
  LoginResponse(this.status,this.message,this.userName,this.profileUrl,this.userId);

  factory LoginResponse.fromJson(map<String,dynamic> json) {
       return LoginResponse(
            status : json['status'],
            message : json['message'],
            userName : json['user_name'],
            profileUrl : json['profile_url'],
            userId : json['user_id']
       )
  }
}

I know, I know, I just want to show you guys, the manual deserialization before showing the auto-generated json deserialization.

Generate an auto-generated json File

The following shows the JsonSerializer model of above json.

import 'package:json_annotation/json_annotation.dart';

@JsonSerializable()
class LoginResponse {
  bool status;
  String message;
  @JsonKey(name: 'user_name')
  String userName;
  @JsonKey(name: 'profile_url')
  String profileUrl;
  @JsonKey(name: 'user_id')
  int userId;

  LoginResponse(
      this.status, this.message, this.userName, this.profileUrl, this.userId);
}

If we want to use JsonSerializer to generate code, we must add the annotation @JsonSerializable() before the signature of class that needs to generate the code and if you need to define the name case of the member, use the @JsonKey annotation.

So, the question is how should the code be generated..? If you guys have remembered that we add the build_runner dependency in our pupspec.yaml file.

So, in order to generate the Pojo class for a LoginResponse run the following command in the current project directory

flutter packages pub run build_runner build

After the command runs successfully, we should be able to find a new file under the entity file.

json_serialization_generated_file

The models.g.dart json parsing file generated by build_runner based on  JsonSerializer. Below is the generated dart file.

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) {
  return LoginResponse(
      json['status'] as bool,
      json['message'] as String,
      json['user_name'] as String,
      json['profile_url'] as String,
      json['user_id'] as int);
}

Map<String, dynamic> _$LoginResponseToJson(LoginResponse instance) =>
    <String, dynamic>{
      'status': instance.status,
      'message': instance.message,
      'user_name': instance.userName,
      'profile_url': instance.profileUrl,
      'user_id': instance.userId
    };

Now we only need to associate our generated file in our entity class and provide a way to parse the json in the entity class. Let’s see how we can associate the generated file with our generated file.

import 'package:json_annotation/json_annotation.dart';
part 'package:flutter_projects/models/models.g.dart';   // associated generated dart file

@JsonSerializable()
class LoginResponse {
  bool status;
  String message;
  @JsonKey(name: 'user_name')
  String userName;
  @JsonKey(name: 'profile_url')
  String profileUrl;
  @JsonKey(name: 'user_id')
  int userId;

  LoginResponse(this.status, this.message, this.userName, this.profileUrl,
      this.userId);

  factory LoginResponse.fromJson(Map<String, dynamic> json) =>
      _$LoginResponseFromJson(json);
}

In order for the entity class file to find the generated file, we need part and let the entity class to mix with the generated file. Finally, a factory constructor is provided, which actually calls  _$LoginResponseFromJson the method of the generated file. The  _$LoginResponseFromJson method is the one who deserializes our json. And that’s how we can simply deserialize our json into dart object.

Let’s take another example where we have a user json and within that user object, we have subjects of a user. Let’s see the json first.

{
  "status" : true,
  "message" : "User successfuly logged in!",
  "user_name" : "Ahsen Saeed",
  "profile_url" : "www.codinginfinite.com",
  "user_id" : 280,
  "subjects" : [
        {
           "subject_name" : "ComputerProgramming"
        },
        {
           "subject_name" : "Calculus"
        }
    ]
}

You see in order to parse the above json, we need to add a list of subjects in our LoginResponse model.

@JsonSerializable()
class LoginResponse {
  bool status;
  String message;
  @JsonKey(name: 'user_name')
  String userName;
  @JsonKey(name: 'profile_url')
  String profileUrl;
  @JsonKey(name: 'user_id')
  int userId;
  @JsonKey(name: 'subjects')
  List<Subject> subjects;

  LoginResponse(this.status, this.message, this.userName, this.profileUrl,
      this.userId, this.subjects);

  factory LoginResponse.fromJson(Map<String, dynamic> json) =>
      _$LoginResponseFromJson(json);
}

@JsonSerializable()
class Subject {
  @JsonKey(name: 'subject_name')
  String subjectName;

  Subject(this.subjectName);

  factory Subject.fromJson(Map<String, dynamic> json) =>
      _$SubjectFromJson(json);
}

Finally, below is the newly generated file.

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) {
  return LoginResponse(
      json['status'] as bool,
      json['message'] as String,
      json['user_name'] as String,
      json['profile_url'] as String,
      json['user_id'] as int,
      (json['subjects'] as List)
          ?.map((e) =>
              e == null ? null : Subject.fromJson(e as Map<String, dynamic>))
          ?.toList());
}

Map<String, dynamic> _$LoginResponseToJson(LoginResponse instance) =>
    <String, dynamic>{
      'status': instance.status,
      'message': instance.message,
      'user_name': instance.userName,
      'profile_url': instance.profileUrl,
      'user_id': instance.userId,
      'subjects': instance.subjects
    };

Subject _$SubjectFromJson(Map<String, dynamic> json) {
  return Subject(json['subject_name'] as String);
}

Map<String, dynamic> _$SubjectToJson(Subject instance) =>
    <String, dynamic>{'subject_name': instance.subjectName};

I hope this article, gives you a good understanding of how to deserialize the json with json_annotation library into plain old dart object. If you’ve enjoyed this story, share this article with flutter community. 

Thank you for being here and keep reading…

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…