Dio + Freezed + Retrofit + json= ❤ in Flutter

Dan Shulgin
4 min readJul 11, 2024

--

Someday, I got stuck with a problem: my API layer responses weren’t that pretty. Some research didn’t yield anything useful, so I decided to write something “new” in some way.

Codegen simplifies your life until its too hard for you to increase app complexity

Prerequisites:

  • Dio : HTTP requests
  • Retrofit: generate all of those requests
  • Freezed: generate data models to work with
  • Json Serializable: generate JSON parsing logic
  • GetIt: dependency injection
  • Build Runner and other generators

Here is pubspec.yaml dep’s part

dependencies:
retrofit: ">=4.0.0 <5.0.0"
dio: ^5.4.3+1
get_it: ^7.7.0
injectable: ^2.4.2
freezed_annotation: ^2.4.1
json_annotation: ^4.9.0

dev_dependencies:
build_runner: ^2.4.9
freezed: ^2.5.2
json_serializable: ^6.8.0
injectable_generator: ^2.6.1
retrofit_generator: ">=7.0.0 <8.0.0"

My backend can give a paginated response which contains all of the pagination data and the data models that are requested. It looks like this:

{
"docs": [
{
"id": "668fc37546d03f5cd6410571",
"price": 1,
"currencyCode": "usd",
"expirationDate": "2039-03-11T12:00:00.000Z",
"purchaseDate": "2024-07-06T12:00:00.000Z",
"createdAt": "2024-07-11T11:35:17.901Z",
"updatedAt": "2024-07-11T12:55:23.194Z",
"user": {
"id": "66808684e5f73b87837cbdc9",
"email": "mymail@mail.com,
"createdAt": "2024-06-29T22:11:16.268Z",
"updatedAt": "2024-07-11T19:45:21.105Z",
"devices": [
{
"deviceId": "...",
"token": "3x7zfi91kfR79o5L"
}
],
"maxDevices": 4
}
}
],
"totalDocs": 1,
"limit": 10,
"totalPages": 1,
"page": 1,
"pagingCounter": 1,
"hasPrevPage": false,
"hasNextPage": false,
"prevPage": null,
"nextPage": null
}

Create base classes

As you can see, the “docs” array can be of any type, so I’ve created this PaginatedResponse class as a base for others. This is pretty obvious.

@freezed
@JsonSerializable()
class MyDocsReponse
with _$MyDocsReponse {
factory MyDocsReponse({
@Default([]) List<dynamic> docs,
required int totalDocs,
required int limit,
required int page,
required int pagingCounter,
required bool hasPrevPage,
required bool hasNextPage,
required int? prevPage,
required int? nextPage,
}) = _MyDocsReponse;

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

But it’s not very flexible in terms of data types, so let’s create the data itself

part 'purchase.freezed.dart';
part 'purchase.g.dart';

@freezed
class Purchase with _$Purchase {
factory Purchase({
required String id,
@Default(0) double price,
@Default('') String currencyCode,
required DateTime expirationDate,
required DateTime purchaseDate,
}) = _Purchase;

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

nothing fancy, just a regular freezed+json serializable class

Create an API

For this task i’ve decided to go with lovely dio+retrofit because:
1) easy to use
2) codegen
3) interceptors
4) a lot of caching packages
5) logging
And last but not least: You can create its BLAZINGLY FAST

@RestApi()
abstract class MyApi {
factory MyApi(Dio dio, {required String baseUrl}) => _MyApi(
dio,
baseUrl: baseUrl,
);


@GET('/purchases')
Future<PayloadDocsReponse> purchases();

}

I have an authentication part as well, but for this example, let’s focus only on one method.

Now we need to register in our di

final GetIt getIt = GetIt.instance;

@InjectableInit(
initializerName: 'init',
preferRelativeImports: true,
asExtension: true,
)
Future<void> configureDependencies() async {
await sharedPrefsDi();

getIt
.........
..registerSingleton<MyApi>(
MyApi(
Dio(BaseOptions(headers: {'Access-Control-Allow-Origin': '*'}))
..interceptors.add(AuthTokenInterceptor()),
baseUrl: 'http://127.0.0.1:3000/api',
),
)


getIt.init();
}

So now we are ready to go and with our api, i mean MyApi

Generics magic

Now is the main part => how to combine generics, codgen, json, dio, retrofit and freezed?

Json Serializable has a very useful field called genericArgumentFactories — it allows us to use JSON parsing methods from our generic classes.

For this, we need to create a base response type for other classes like Purchases.

abstract class MyDocType {}

We have extended our data classes from it.

part 'purchase.freezed.dart';
part 'purchase.g.dart';

@freezed
class Purchase extends MyDocType with _$Purchase {
factory Purchase({
required String id,
@Default(0) double price,
@Default('') String currencyCode,
required DateTime expirationDate,
required DateTime purchaseDate,
}) = _Purchase;

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

THE MAIN PART

And add a custom fromJson method to our Pagination class and add a generic type. It isn't that fancy, sorry :)

part 'my_doc_response.freezed.dart';
part 'my_doc_response.g.dart';

@freezed
@JsonSerializable(genericArgumentFactories: true)
class MyDocResponse<T extends MyDocType>
with _$MyDocResponse<T> {
factory MyDocResponse({
@Default([]) List<T> docs,
required int totalDocs,
required int limit,
required int page,
required int pagingCounter,
required bool hasPrevPage,
required bool hasNextPage,
required int? prevPage,
required int? nextPage,
}) = _MyDocResponse<T>;

factory MyDocResponse.fromJson(
Map<String, dynamic> json,
) {
late final T Function(Object?) fromJsonT;

switch (T) {
case Purchase:
fromJsonT =
(Object? o) => Purchase.fromJson(o! as Map<String, dynamic>) as T;
break;
default:
throw UnimplementedError(
'MyDocResponse $T type is not implemented');
}
return _$MyDocResponseFromJson<T>(json, fromJsonT);
}
}

And you have to specify each of your generic types that can be parsed from json in its fromJson switch case like above

Dont forget to add generic return types in your retrofit config file

@RestApi()
abstract class MyApi {
factory MyApi(Dio dio, {required String baseUrl}) => _MyApi(
dio,
baseUrl: baseUrl,
);

@GET('/purchases')
Future<PayloadDocsReponse<Purchase>> purchases();

}

Make sure you work with exceptions corretly and hadle them.

If you have any questions or remarks — please write in the comments, ty and see ya.

So we have:
1) Flexible request data models
2) 1 command to generate data, requets ets
3) All of the benefits from all those libraries

--

--

Dan Shulgin
0 Followers

Just a Senior Flutter Developer that loves jdm cars 🤷