Unlocking Efficiency š« : Building a Generic API Response Template in Dart with JSON Serializable and Retrofit
Letās Suppose all your API responses come in the form
{
"message": "Request processed successfully",
"errorCode": "200",
"success": true,
"serverTime": "2024-02-04T12:30:00Z",
"data": {
"result": "Example data",
"details": {
"info1": "Information 1",
"info2": "Information 2"
}
}
}
In scenarios where our API responses consistently encapsulate data within a ādataā field, deserializing the data requires parsing the overall API response first. While creating separate model classes for API responses and data is a common approach, it can become repetitive and cumbersome for various data types.
To address this, Dartās json_serializable package offers a solution. By utilizing its features, we can design a generic class that encompasses the necessary fields and facilitates seamless deserialization of data, regardless of its type.
The magic of the json_serializable is that it generates additional āhelperā parameters for the fromJson and toJson methods, enabling smooth serialization and deserialization of values associated with those generic types.
Setting the Stage
Dependencies: š©
Before diving into the code, make sure to include the necessary dependencies in your pubspec.yaml file
dependencies:
json_annotation: ^4.0.1
json_serializable: ^4.5.0
retrofit: ^4.1.0
dev_dependencies:
build_runner: ^2.1.7
retrofit_generator: ^7.0.1
Run flutter pub get to fetch the dependencies.
Code Generation: āļø
Create a new Dart file for the API response template base_response.dart. Weāll leverage code generation, so add the part directive for base_response.g.dart to store the generated code.
Our base response class hence will look like this.
import 'package:json_annotation/json_annotation.dart';
part 'base_response.g.dart';
@JsonSerializable(genericArgumentFactories: true)
class BaseAPIResponse<T> {
final String? message;
final String? errorCode;
final bool? success;
final int? serverTime;
final T? data;
BaseAPIResponse(
{this.message, this.errorCode, this.success, this.serverTime, this.data});
factory BaseAPIResponse.fromJson(
Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
_$BaseAPIResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object Function(T value) toJsonT) =>
_$BaseAPIResponseToJson(this, toJsonT);
}
- genericArgumentFactories: true is responsible for supporting serialzation of generic types
- The first factory method is responsible for creating an instance of BaseAPIResponse<T> from a JSON map. It takes two parameters:
- Map<String, dynamic> json: The JSON data received from the API response.
- T Function(Object? json) fromJsonT: A function that will be used to deserialize the data field of type T. The fromJsonT function is responsible for converting the JSON data into the actual type T.
3. The second method converts an instance of BaseAPIResponse<T> to a JSON map. It takes a single parameter:
- Object Function(T value) toJsonT: A function responsible for serializing the data field of type T into JSON. The toJsonT function is expected to handle the serialization of the specific type T.
This is the magic of this package as it creates these implementation methods _$BaseAPIResponseFromJson, _$BaseAPIResponseToJson, during code generation.
Now we can run
flutter pub run build_runner build --delete-conflicting-outputs
The generated file will look like this which will handle the serialization/deserialization of the generic type.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'base_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BaseAPIResponse<T> _$BaseAPIResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
BaseAPIResponse<T>(
message: json['message'] as String?,
errorCode: json['errorCode'] as String?,
success: json['success'] as bool?,
serverTime: json['serverTime'] as int?,
data: _$nullableGenericFromJson(json['data'], fromJsonT),
);
Map<String, dynamic> _$BaseAPIResponseToJson<T>(
BaseAPIResponse<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'message': instance.message,
'errorCode': instance.errorCode,
'success': instance.success,
'serverTime': instance.serverTime,
'data': _$nullableGenericToJson(instance.data, toJsonT),
};
T? _$nullableGenericFromJson<T>(
Object? input,
T Function(Object? json) fromJson,
) =>
input == null ? null : fromJson(input);
Object? _$nullableGenericToJson<T>(
T? input,
Object? Function(T value) toJson,
) =>
input == null ? null : toJson(input);
Notice how easy it got to just wrap our model class with BaseResponse and now we can again run the build runner to generate the api_service.g file
If we look closely we can find a code snippet
final value = BaseAPIResponse<TicketFeedbackModel>.fromJson(
_result.data!,
(json) => TicketFeedbackModel.fromJson(json as Map<String, dynamic>),
);
Retrofit uses the BaseAPIResponse.fromJson and provides the TicketFeedBackModel.fromJson to create Object of type BaseAPIResponse<TicketFeedBackModel>
So cool! Right? š²
Now we can directly use
final <BaseAPIResponse<TicketFeedbackModel>> response=
await sendFeedback(request: request, tId: ticketId);
if(response.success==true){
return response.data?? NullResponseData();
}
else{
throw(Exception([response.message, response.errorCode])
}
To sum up, leveraging Dartās JSON Serializable and Flutter Retrofit enables the creation of a generic API response template. This approach streamlines response handling, promotes code reusability, and enhances overall development efficiency.
Happy Coding! ā