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.
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.
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);
}
}
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),
),
);
}
}
final List posts = json.decode(snapshot.data.bodyString);
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.
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.
If you are using StateFul widget, you can dispose the service on the widgets dispose method
How can we create a common chopper client for all services? I am very much new to flutter please guide.
This is also my question.
Please, Sir Reso Coder.
How to use the same chopper clients for multiple endpoints or services ?
What about bigger json structures? What if I have a list of objects that contain can contain other objects?
It will all get handled by the built_value package.
Please, how to use multiple service and one chopper client
Hi, I wonder is it possible to have multiple BASE_URL ? Do I need to client another client and service for that?
hey matt first of tank you very much for tee mazing courses ..so my problem is when I’m setting builder I get this error
Compiler message:
lib/main.dart:11:7: Error: A function expression can’t have a name.
builder(_) => PostApi.create(),
^^^^^^^
lib/main.dart:10:20: Error: Too many positional arguments: 0 allowed, but 1 found.
Try removing the extra positional arguments.
return Provider(
^
/C:/flutter/.pub-cache/hosted/pub.dartlang.org/provider-4.1.3/lib/src/provider.dart:123:3: Context: Found this candidate, but the arguments don’t match.
Provider({
^^^^^^^^
Reload already in progress, ignoring request