5

Chopper (Retrofit for Flutter) #1 – Basics

Working with RESTful APIs and making HTTP requests is the bread and butter of almost every developer. If you're coming from Android, you probably know about Retrofit. iOS developers, as I'm told, have the Alamofire library.

On Flutter though, you usually use the http package or something like dio. Although these packages do an awesome job, they still leave you working at the lowest level. The question arises - what can we, Flutter developers, use to simplify our work with HTTP APIs? Chopper

Setting up the project

Chopper is a library which, besides other things, generates code to simplify the development process for us. The chopper_generator is only a development dependency - you don't need to package it into the final app.

In addition to Chopper itself, we are going to use the Provider package to make InheritedWidget syntax simple.

pubspec.yaml

...
dependencies:
  flutter:
    sdk: flutter
  chopper: ^2.4.0
  provider: ^3.0.0+1

...

dev_dependencies:
  flutter_test:
    sdk: flutter
  chopper_generator: ^2.3.4
  # No version number means the latest version
  build_runner:
...

Choosing a REST API

Before we can continue, we have to choose an API to work with. This project will use the JSONPlaceholder API. It is built specifically for these kinds of learning purposes. This API provides 100 fake posts containing a title and a body of the text.

This first part of the Chopper series and also the upcoming ones will deal with these fake posts. In this part, we're going to build a basic Flutter app showing a list of all posts and also a single post "detail".

The finished "Chopper Blog" app

Creating a ChopperService

Most of the code you write with Chopper is inside a subclass of ChopperService. In this tutorial, we're going to have a PostApiService which needs to be an abstract class containing only the definitions of its methods. Then the chopper_generator will step in, look at those definitions and generate all of the boilerplate for us.

post_api_service.dart

import 'package:chopper/chopper.dart';

// Source code generation in Dart works by creating a new file which contains a "companion class".
// In order for the source gen to know which file to generate and which files are "linked", you need to use the part keyword.
part 'post_api_service.chopper.dart';

@ChopperApi(baseUrl: 'https://jsonplaceholder.typicode.com/posts')
abstract class PostApiService extends ChopperService {
  @Get()
  Future<Response> getPosts();

  @Get(path: '/{id}')
  // Query parameters are specified the same way as @Path
  // but obviously with a @Query annotation
  Future<Response> getPost(@Path('id') int id);

  // Put & Patch requests are specified the same way - they must contain the @Body
  @Post()
  Future<Response> postPost(
    @Body() Map<String, dynamic> body,
  );
}

The file above contains all the code we need for the source gen to generate the the implementation.

Quick note on HTTP headers

While we are not going to use any headers with the JSONPlaceholder API, in most real apps, knowing how to work with headers is a must.

Headers are a way to pass additional data to the server bundled in the request. Authentication is one example where data is sent through headers.
A header is a key-value pair (e.g. Authorization: Bearer 123456).

If we were to add headers to the getPosts() method, it would look like the following code.

post_api_service.dart

// Headers (e.g. for Authentication) can be added in the HTTP method constructor
// or also as parameters of the Dart method itself.
@Get(headers: {'Constant-Header-Name': 'Header-Value'})
Future<Response> getPosts([
  // Parameter headers are suitable for ones which values need to change
  @Header('Changeable-Header-Name') String headerValue,
]);

Generating code

The PostApiService class is abstract - its implementation will be generated by the chopper_generator package. To initiate code generation, we need to run a command in the terminal.  

The last part of the command can be either "build" or "watch". We're using watch, so that the generation runs automatically whenever we change content of the post_api_service.dart file.

flutter packages pub run build_runner watch

Running this command has generated a new file named post_api_service.chopper.dart.

Instantiating a ChopperClient

Having just the methods specifying what to fetch is simply not enough. We also need to have a means of fetching. That is, we need an HTTP client. The Chopper library uses its own ChopperClient which is built on top of the Dart's own client from the http package.

How are we going to make the PostApiService work together with the ChopperClient though? After all, those three methods getPosts, getPost and postPost which are there to simplify work with the HTTP API, have to make requests using the client.

The solution is simple - if you take a look at the generated class (called _$PostApiService), its constructor takes in an optional parameter of type ChopperClient. The generated methods then use this client to make HTTP requests.

post_api_service.chopper.dart

...
class _$PostApiService extends PostApiService {
  _$PostApiService([ChopperClient client]) {
    if (client == null) return;
    this.client = client;
  }
  ...
}

Basically, what we need to operate with from other parts of the code is the initialized instance of _$PostApiService. The most elegant way to get to it is through a static method on the PostApiService.

post_api_service.dart

import 'package:chopper/chopper.dart';

part 'post_api_service.chopper.dart';

// This baseUrl is now changed to specify only the endpoint '/posts'
@ChopperApi(baseUrl: '/posts')
abstract class PostApiService extends ChopperService {
  ...

  static PostApiService create() {
    final client = ChopperClient(
      // The first part of the URL is now here
      baseUrl: 'https://jsonplaceholder.typicode.com',
      services: [
        // The generated implementation
        _$PostApiService(),
      ],
      // Converts data to & from JSON and adds the application/json header.
      converter: JsonConverter(),
    );

    // The generated class with the ChopperClient passed in
    return _$PostApiService(client);
  }
}
Notice how the baseUrl in the @ChopperApi annotation changed to just '/posts'. The bulk of the URL is now defined in the ChopperClient. This is a good practice which allows you to have multiple ChopperServices for different endpoints of the same API.

Building the UI

Since this is only a simple app for showcasing Chopper, we don't want to bog ourselves down with any real state management. You can learn about proper state management with BLoC from a separate tutorial.

The UI will consist of two pages - home & single post. We're going to use the Provider package to easily pass the PostApiService between pages. The provider widget will wrap the root MaterialApp.

main.dart

import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'data/post_api_service.dart';
import 'home_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      // The initialized PostApiService is now available down the widget tree
      builder: (_) => PostApiService.create(),
      // Always call dispose on the ChopperClient to release resources
      dispose: (context, PostApiService service) => service.client.dispose(),
      child: MaterialApp(
        title: 'Material App',
        home: HomePage(),
      ),
    );
  }
}

HomePage

The main HomePage widget will display a list of posts and a FloatingActionButton which will demonstrate the POST request. Upon tapping on a specific post, the user will be taken to the SinglePostPage, which we're going to create next.

home_page.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:chopper/chopper.dart';
import 'package:provider/provider.dart';

import 'data/post_api_service.dart';
import 'single_post_page.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chopper Blog'),
      ),
      body: _buildBody(context),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () async {
          // The JSONPlaceholder API always responds with whatever was passed in the POST request
          final response = await Provider.of<PostApiService>(context)
              .postPost({'key': 'value'});
          // We cannot really add any new posts using the placeholder API,
          // so just print the response to the console
          print(response.body);
        },
      ),
    );
  }

  FutureBuilder<Response> _buildBody(BuildContext context) {
    // FutureBuilder is perfect for easily building UI when awaiting a Future
    // Response is the type currently returned by all the methods of PostApiService
    return FutureBuilder<Response>(
      // In real apps, use some sort of state management (BLoC is cool)
      // to prevent duplicate requests when the UI rebuilds
      future: Provider.of<PostApiService>(context).getPosts(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          // Snapshot's data is the Response
          // You can see there's no type safety here (only List<dynamic>)
          final List posts = json.decode(snapshot.data.bodyString);
          return _buildPosts(context, posts);
        } else {
          // Show a loading indicator while waiting for the posts
          return Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }

  ListView _buildPosts(BuildContext context, List posts) {
    return ListView.builder(
      itemCount: posts.length,
      padding: EdgeInsets.all(8),
      itemBuilder: (context, index) {
        return Card(
          elevation: 4,
          child: ListTile(
            title: Text(
              posts[index]['title'],
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            subtitle: Text(posts[index]['body']),
            onTap: () => _navigateToPost(context, posts[index]['id']),
          ),
        );
      },
    );
  }

  void _navigateToPost(BuildContext context, int id) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => SinglePostPage(postId: id),
      ),
    );
  }
}
In this line (located in the FutureBuilder): 
final List posts = json.decode(snapshot.data.bodyString);
You can see there's no type safety. The list of posts is simply List<dynamic>.
Chopper can overcome this if you integrate it with BuiltValue or JSON Serializable. You're going to learn how to do that later in the series.

SinglePostPage

This page will obtain the ID of the post which should be displayed through its constructor. Then, it will call getPost() again with the help of a FutureBuilder.

single_post_page.dart

import 'dart:convert';

import 'package:chopper/chopper.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'data/post_api_service.dart';

class SinglePostPage extends StatelessWidget {
  final int postId;

  const SinglePostPage({
    Key key,
    this.postId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Chopper Blog'),
      ),
      body: FutureBuilder<Response>(
        future: Provider.of<PostApiService>(context).getPost(postId),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            final Map post = json.decode(snapshot.data.bodyString);
            return _buildPost(post);
          } else {
            return Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
    );
  }

  Padding _buildPost(Map post) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: <Widget>[
          Text(
            post['title'],
            style: TextStyle(
              fontSize: 30,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8),
          Text(post['body']),
        ],
      ),
    );
  }
}

Conclusion

We've built an app displaying posts from an API. We've only covered the basics of Chopper though. There's more to learn including things like interceptors and custom converters. By the end of this Chopper series, you'll know how to use Chopper to allow for type safety in a simple way.

Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

  • Cole Phelps says:

    Thanks for this great tutorial.
    I tried chopper in one of my project and is working great. I’m a beginner in flutter development.
    Now I’m trying to implement bloc pattern for the same project, following your bloc tutorial on weather app ofcourse 😀.
    But one thing I can’t figure out is, where to initialise chopper and where to dispose it.
    My app is a shopping cart. So far I have loaded list of products from my own API (without bloc).
    Now I want to use bloc for the same and later I have to show details of single product too.

    Another problem I faced was when device is not online, widget shows null pointer exception. I handed that case by showing no internet in a text widget. I also tried adding a retry button, but when I call bulid(context) in the onPressed, nothing happens. What method should I call to reload the page? I’m using stateless widgets only. I didn’t find the need to use stateful widget so far in this app.

  • Riyaz Shaikh says:

    How can we create a common chopper client for all services? I am very much new to flutter please guide.

  • Erik says:

    What about bigger json structures? What if I have a list of objects that contain can contain other objects?

  • >