Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (2024)

Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (1)

You now know almost all the thingsChopperhas to offer which you'll need on a daily basis. Sendingrequests, obtainingresponses, addinginterceptors... There's only one thing missing which will plug a hole in a clean coder's heart -type safety.

JSON itself is not typesafe, we'll have to live with it. However, we can make our code a lot more readable and less error prone if weditch dynamic datafor a real data type. Chopper offers an amazing way toconvert request and response datawith the help of a library of your own choice. Our choice for for this tutorial will bebuilt_value.

Chopper Series

  1. Interceptors
  2. Converters (with BuiltValue)

Built Value is arguably the best choice when you want to create immutable data classes (with all the bells and whistles like copying and value equality) AND on top of that, it has a first-class JSON serialization support. You can learn more about this library from the tutorial below.

dart, flutter, tutorial

Making a data class

The first step in making code type-safe is to, well... add some types. Since we will be using built_value, let's first add it as a dependency.

pubspec.yaml

dependencies: ... built_value: ^6.7.0dev_dependencies: ... built_value_generator: ^6.7.0

Throughout this series we've been creating an app for viewing posts from the JSON Placeholder API. Just in case you need a little refresher on how the JSON response looks like, here it is:

GET /posts

[ { "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" }, // 99 additional objects in the list...]

Creating a BuiltPost class

Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (3)

We're interested in all the fieldsexceptuserId. Let's now create aBuiltPostclass which will hold all of this data. Similar to Chopper itself, Built Value also utilizes source generation, so we'll create a new filebuilt_post.dartand the actual implementation will be inside a generatedbuilt_post.g.dartfile. To keep things organized, we'll even put all this into a newmodel folder.

built_post.dart

import 'package:built_value/built_value.dart';import 'package:built_value/serializer.dart';part 'built_post.g.dart';abstract class BuiltPost implements Built<BuiltPost, BuiltPostBuilder> { // IDs are set in the back-end. // In a POST request, BuiltPost's ID will be null. // Only BuiltPosts obtained through a GET request will have an ID. @nullable int get id; String get title; String get body; BuiltPost._(); factory BuiltPost([updates(BuiltPostBuilder b)]) = _$BuiltPost; static Serializer<BuiltPost> get serializer => _$builtPostSerializer;}

This is a pretty standard Built Value data class, but in case you didn't watch the separate built_value tutorial(you should!), here's a quick run down:

  • Fields are get-only properties - data will actually be stored in the generated class.

  • The default constructor is private, there's a factory taking in a Builder instead.

    • Fields' values are set through the Builder.

  • Specifying a serializer property generates a class _$BuiltPostSerializer, which is what we'll use to convert that ugly dynamic data into our beautiful BuiltPost data class.

Adding BuiltPost to global serializers

Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (4)

Yes, once we generate code, we'll have a serializer forBuiltPostclasses. It turns out though that this is not enough! There's an entire ecosystem ofother serializersfor types likeinteger, String, booland other primitives.

To successfully serialize and deserialize BuiltPost, our app will have to use it in conjunction with other serializers. We can accomplish this by addingBuiltPost's serializer to the list of all serializers built_value has to offer.

serializers.dart

import 'package:built_value/serializer.dart';import 'package:built_value/standard_json_plugin.dart';import 'built_post.dart';part 'serializers.g.dart';@SerializersFor(const [BuiltPost])final Serializers serializers = (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

Make sure you add the StandardJsonPlugin whenever you want to use the generated JSON with a RESTful API. By default, BuiltValue's JSON output aren't key-value pairs, but instead a list containing [key1, value1, key2, value2, ...]. This is not what most of the APIs expect.

Generating code

As usual, to initiate source generation, run the following in the terminal. We'll use thewatch command to continue building code down the line when we change PostApiService implementation.

flutter packages pub run build_runner watch

Updating the PostApiService

We want the methods inside PostApiService to return Responses which hold either a list (BuiltList in this case) of BuiltPosts, or just a single BuiltPost. To make conversion to BuiltValue classes possible, we are going to create a BuiltValueConverter later on. Even though it doesn't yet exist, replace the default JsonConverter with it just so that we won't have to come back to this file.

post_api_service.dart

import 'package:chopper/chopper.dart';import 'package:built_collection/built_collection.dart';import 'package:retrofit_prep/model/built_post.dart';import 'built_value_converter.dart';part 'post_api_service.chopper.dart';@ChopperApi(baseUrl: '/posts')abstract class PostApiService extends ChopperService { @Get() // Update the type parameter of Response to BuiltList<BuiltPost> Future<Response<BuiltList<BuiltPost>>> getPosts(); @Get(path: '/{id}') // For single returned objects, response will hold only one BuiltPost Future<Response<BuiltPost>> getPost(@Path('id') int id); @Post() Future<Response<BuiltPost>> postPost( @Body() BuiltPost post, ); static PostApiService create() { final client = ChopperClient( baseUrl: 'https://jsonplaceholder.typicode.com', services: [ _$PostApiService(), ], // Our own converter for built values built on top of the default JsonConverter converter: BuiltValueConverter(), // Remove all interceptors from the previous part except for the HttpLoggingInterceptor // which is always useful. interceptors: [ HttpLoggingInterceptor(), ], ); return _$PostApiService(client); }}

BuiltValueConverter

Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (5)

After all this setup comes the part of this tutorial you are the most interested in -how do you "connect" Chopper and BuiltValue to work together?By creating a BuiltValueConverter.

Wewon'thave to build it from scratch though, as we will utilize thebinary data to dynamic Map/Listconversion which the defaultJsonConverterprovides. Still, there's a lot of coding ahead! Let's first overrideconvertRequestmethod as it's alotless tricky thanconvertResponse.

Of course, we are building this code to work generically with all the classes implementing Built, not just the BuiltPost.

Request conversion

built_value_converter.dart

import 'package:chopper/chopper.dart';import 'package:built_collection/built_collection.dart';import 'package:retrofit_prep/model/serializers.dart';class BuiltValueConverter extends JsonConverter { @override Request convertRequest(Request request) { return super.convertRequest( request.replace( // request.body is of type dynamic, but we know that it holds only BuiltValue classes (BuiltPost). // Before sending the request to the network, serialize it to a List/Map using a BuiltValue serializer. body: serializers.serializeWith( // Since convertRequest doesn't have a type parameter, Serializer's type will be determined at runtime serializers.serializerForType(request.body.runtimeType), request.body, ), ), ); }}

Request classes do not (yet) use generic type parameters for their bodies, hence they're dynamic. We know, however, that the body of a request will always be an instance of BuiltPost or some other Built class, should we add one. Before sending out the request, we have to serialize the BuiltPost body to a Map which will subsequently get converted to JSON by Chopper.

You could also let BuiltValue determine the serializer's type itself by calling

serializers.serialize(request.body)

but that would bloat the request body by adding unnecessary type-related data to it.

Response conversion

Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (6)

Converting dynamic responses which contain either aList of Mapsor just aMapitself is a bit of a tougher task. We will have to differentiate between the cases of deserializing a List and a single Map. Also, what if weexplicitlyset a method in the ChopperService toreturnaMap? You know, we might not always want to use BuiltValue data classes...

To accomplish all of this while keeping the code clean, we will separate the conversion and deserialization into multiple methods. Also, as opposed to convertRequest,convertResponsedoes have type parameters, so we won't have to determine anything at runtime.

Before we add all of the code though, those type parameters require a brief explanation. The definition of convertResponseis the following:

Response<BodyType> convertResponse<BodyType, SingleItemType>
  • BodyTypewill be a BuiltValue class, in our case it's either BuiltPost or BuiltList<BuiltPost>.
  • If a body of the response contains only a single object, BodyType and SingleItemType will be identical.
  • If the body contains a list of objects, Chopper will set the SingleItemType to be, well, the type which the list contains.

built_value_converter.dart

...class BuiltValueConverter extends JsonConverter { ... @override Response<BodyType> convertResponse<BodyType, SingleItemType>( Response response) { // The response parameter contains raw binary JSON data by default. // Utilize the already written code which converts this data to a dynamic Map or a List of Maps. final Response dynamicResponse = super.convertResponse(response); // customBody can be either a BuiltList<SingleItemType> or just the SingleItemType (if there's no list). final BodyType customBody = _convertToCustomObject<SingleItemType>(dynamicResponse.body); // Return the original dynamicResponse with a no-longer-dynamic body type. return dynamicResponse.replace<BodyType>(body: customBody); } dynamic _convertToCustomObject<SingleItemType>(dynamic element) { // If the type which the response should hold is explicitly set to a dynamic Map, // there's nothing we can convert. if (element is SingleItemType) return element; if (element is List) return _deserializeListOf<SingleItemType>(element); else return _deserialize<SingleItemType>(element); } BuiltList<SingleItemType> _deserializeListOf<SingleItemType>( List dynamicList, ) { // Make a BuiltList holding individual custom objects return BuiltList<SingleItemType>( dynamicList.map((element) => _deserialize<SingleItemType>(element)), ); } SingleItemType _deserialize<SingleItemType>( Map<String, dynamic> value, ) { // We have a type parameter for the BuiltValue type // which should be returned after deserialization. return serializers.deserializeWith<SingleItemType>( serializers.serializerForType(SingleItemType), value, ); }}

Quite a lot of code, I know. Remember though, that once you write a converter like this which works with Chopper, you actually spare yourself of a lot of boilerplate in the long run.

Let's now change the UI part of the app code to use Chopper with type safety!

Updating the UI widgets

The look of the app will remain unchanged. We only want to finally use the Built​Post data class instead of dynamic data.

Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (7)

UI remains unchanged

In the HomePage, we perform a POST request from the floating action button. Instead of a Map, we can now pass a BuiltPost object into the request.

home_page.dart

...floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () async { // Use BuiltPost even for POST requests final newPost = BuiltPost( (b) => b // id is null - it gets assigned in the backend ..title = 'New Title' ..body = 'New body', ); // The JSONPlaceholder API always responds with whatever was passed in the POST request final response = await Provider.of<PostApiService>(context).postPost(newPost); // We cannot really add any new posts using the placeholder API, // so just print the response to the console print(response.body); },),...

As for the GET request on the/posts endpoint, we will change the type parameter of the FutureBuilder and also ditch the unsafe accessors of a dynamic map in favor of regular accessors of an object. Also, now we don't have to sprinkle JSON conversion logic throughout our UI, which is definitely a good thing.

home_page.dart

import 'package:flutter/material.dart';import 'package:chopper/chopper.dart';import 'package:provider/provider.dart';import 'package:built_collection/built_collection.dart';import 'data/post_api_service.dart';import 'model/built_post.dart';import 'single_post_page.dart';class HomePage extends StatelessWidget { ... FutureBuilder<Response> _buildBody(BuildContext context) { // Specify the type held by the Response return FutureBuilder<Response<BuiltList<BuiltPost>>>( future: Provider.of<PostApiService>(context).getPosts(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { // Exceptions thrown by the Future are stored inside the "error" field of the AsyncSnapshot if (snapshot.hasError) { return Center( child: Text( snapshot.error.toString(), textAlign: TextAlign.center, textScaleFactor: 1.3, ), ); } //* Body of the response is now type-safe and of type BuiltList<BuiltPost>. final posts = snapshot.data.body; return _buildPosts(context, posts); } else { // Show a loading indicator while waiting for the posts return Center( child: CircularProgressIndicator(), ); } }, ); } // Changed the parameter type. ListView _buildPosts(BuildContext context, BuiltList<BuiltPost> 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), ), ); }, ); } ...}

As for the page displaying a single post, the changes will be similar to the ones above. Just don't useBuiltList<BuiltPost> but only aBuiltPostinstead.

single_post_page.dart

import 'package:chopper/chopper.dart';import 'package:flutter/material.dart';import 'package:provider/provider.dart';import 'data/post_api_service.dart';import 'model/built_post.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<BuiltPost>>( future: Provider.of<PostApiService>(context).getPost(postId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { final post = snapshot.data.body; return _buildPost(post); } else { return Center( child: CircularProgressIndicator(), ); } }, ), ); } Padding _buildPost(BuiltPost 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

Adding type safety to Chopper is probably the most challenging task regarding this library. In this tutorial, you learned how to integrate Built Value together with Chopper to make your code more robust and easier to read. Consider subscribing and joining the newsletter if you want to grow your Flutter skills!

About the author

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a freelancer and most importantly developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

Riverpod 2.0 – Complete Guide (Flutter Tutorial)

Riverpod 2.0 – Complete Guide (Flutter Tutorial)

  • Another great tutorial. Thank you. I learnt a non documented library, also I realized the importance of built packages.

    Reply

  • This is great! However, I’m having trouble with errors using this approach. The Chopper service is expecting a Response with the type of the built value, but is getting a Response in the case of an error response. Any suggestions?

    Reply

  • hi, thank you for your great tutorials. but I have a question here. imagine in a scenario we have two objects that the second one is used as a type for one of the fields in the first one. for using built_value we have to use abstract class, but we can’t use it as a type! what should we do here?

    Reply

  • Hi @matej,

    I appreciate the great post.

    How would i write a custom serializer for a scenario like this one:

    In an api response, a certain object returns this dictionary: “Images”: { “Key1”: “value1”, “Key2”: “value2”, “Key3”: “value3” }

    And another object returns; “Images”: { “Key1”: “value1”, “Key2”: “value2”, “Key3”: “value3”, “Key4”: “value4” },

    And both objects are supposed to be deserialized using the same method for automation to be possible.

    I know a foreach loop would deserialize the data properly but I am failing to write a custom Serializer to fit this purpose.

    Thanks for the help in advance

    Reply

  • Hi Matt Rešetár!

    Great tutorial! I only have a small suggestion:

    I’ve run into some problems when deserializing a list of integers using your BuiltValueConverter. The problem is that your _deserialize method expects a Map, and when the code processes a list of primitives, like integers, this fails.

    So to process responses like [1, 2, 3, 4, 5], we need to modify your _deserializeListOf() method a little bit, and add a safe-check to make sure that our list-elements are not already in the state we want them to be.

    So this is how I modified your code to make it more generic:

    dynamicList.map((element) {
    if (element is SingleItemType) return element;
    return _deserialize(element);
    }),

    This will instantly return primitive items, without trying to call the _deserialize() method on them.

    Reply

  • What is BuiltList?

    Reply

  • I am getting The argument type ‘Serializer?’ can’t be assigned to the parameter type ‘Serializer’. when i am creating the json custom converter for built value. I tried your code but i am getting this error. Please guide.

    import ‘package:chopper/chopper.dart’;
    import ‘package:chopper_project/serializers.dart’;

    class BuiltValueConverter extends JsonConverter {
    @override
    Request convertRequest(Request request) {
    final requestToReturn = request.copyWith(
    body: serializers.serializeWith(
    serializers.serializerForType(request.body.runtimeType),
    request.body,
    ),
    );
    return super.convertRequest(requestToReturn);
    }
    }

    Reply

    • Try:
      serializers.serializerForType(request.body.runtimeType)!

      Reply

  • How can we send list of int in request?

    Reply

  • Chopper (Retrofit for Flutter) #3 – Converters & Built Value Integration - Reso Coder (2024)

    FAQs

    What is chopper in Flutter? ›

    Chopper is a package for making API calls in Flutter apps. Chopper generates code that makes it easy to define API endpoints as Dart functions with strongly typed request and response bodies. It also supports interceptors for adding headers, logging requests and responses, and caching responses.

    What is the difference between Dio and retrofit? ›

    It is a powerful and feature-rich networking library built on top of Dio and inspired by Android's Retrofit. It generates boilerplate code, offering advanced features and tight integration with other libraries.

    How do you call API in Flutter using retrofit? ›

    Let's start the steps for Retrofit API calling in a flutter
    1. Step 1: Create a flutter project.
    2. Step 2: Add the below dependencies in pubspec.yaml file.
    3. Step 3: Now, let's create an abstract API request class.
    4. Step 4: Now run the command in the terminal.
    5. flutter pub run build_runner build.
    Dec 15, 2023

    What are the three types of chopper? ›

    Forced commutated chopper is further classified as Jones chopper, Morgan chopper. Based on output voltage values, choppers are classified into the step-down chopper, step-up chopper, and step up/down chopper. Based on the power loss at switching time, choppers are classified as Hard switched and soft switched.

    What is the purpose of a chopper? ›

    A chopper is a static device that converts fixed DC input voltage to a variable DC output voltage directly. A chopper is considered as DC equivalent of an AC transformer since it behaves in an identical manner. The other name of chopper is DC transformer.

    Why should we use retrofit? ›

    Retrofit is a powerful and popular library that simplifies this process by providing a clean and efficient way to make API calls. Retrofit is a type-safe HTTP client for Android and Java — developed by Square who developed Dagger, Okhttp, etc.

    Why do we use retrofit? ›

    Retrofit provides a convenient builder for constructing our required object. It needs the base URL which is going to be used for every service call and a converter factory – which takes care of the parsing of data we're sending and also the responses we get.

    How does retrofit work? ›

    The Retrofit library is a type-safe REST client for Android, Java, and Kotlin, developed by Square. With the help of the Retrofit library, we can have access to a powerful framework that helps us in authenticating and interacting with APIs and sending network requests with OkHttp.

    Why use retrofit in Flutter? ›

    In the tempestuous flow of app development, combining the power of Flutter and Retrofit can greatly enhance your efficacy. Flutter is an open-source UI software development kit, while Retrofit, a type-safe HTTP client, aids in networking requests while using REST APIs.

    What is the difference between Dio and retrofit in Flutter? ›

    retrofit is a HTTP client made around dio, used to generate dio functions from your API definition. dio is Flutter's most popular HTTP client. json_annotation defines the annotations used by json_serializable (e.g. @JsonSerializable )

    How do you integrate a Flutter app with an API? ›

    ‍REST API integration in the Flutter app
    1. Get the base URL, the endpoints, and the API key.
    2. Add required packages in-app to consume HTTP resources like http, dio, chopper, etc.
    3. Create a constant file that will hold all your URLs and Endpoints.
    4. Parse JSON file to get the Object out of the JSON response.
    Jan 25, 2024

    What is an example of a chopper? ›

    Some examples of DC choppers include: Step-down chopper: A step-down chopper is used to reduce the DC voltage level. It is also known as a buck chopper. Step-up chopper: A step-up chopper is used to increase the DC voltage level.

    What does chopper mean in hip hop? ›

    The word "chopper" was first used in street and hip hop slang to refer to an automatic firearm. The word has also been used as an informal word for helicopter. The linkage to automatic firearms and helicopters are the rapid "tat-tat-tat" sound they make. This may have an analogous relation to fast-paced rap.

    What is known as chopper? ›

    A chopper is a static device which is used to obtain a variable dc voltage from a constant dc voltage source. Also known as a dc-to-dc converter. They can step up the DC voltage or step down the DC voltage levels.

    References

    Top Articles
    Latest Posts
    Article information

    Author: Edwin Metz

    Last Updated:

    Views: 6512

    Rating: 4.8 / 5 (58 voted)

    Reviews: 89% of readers found this page helpful

    Author information

    Name: Edwin Metz

    Birthday: 1997-04-16

    Address: 51593 Leanne Light, Kuphalmouth, DE 50012-5183

    Phone: +639107620957

    Job: Corporate Banking Technician

    Hobby: Reading, scrapbook, role-playing games, Fishing, Fishing, Scuba diving, Beekeeping

    Introduction: My name is Edwin Metz, I am a fair, energetic, helpful, brave, outstanding, nice, helpful person who loves writing and wants to share my knowledge and understanding with you.