visit
Hey! I'll start with the main thing - I'm a lazy person. I'm a very, very lazy developer. I have to write a lot of code - both for the backend and for the frontend. And my laziness constantly torments me, saying: You could not write this code, but you write ... This is how we live.
But what to do? How can you get rid of the need to write at least part of the code? There are many approaches to solving this problem. Let's take a look at some of them.
import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';
part 'example.g.dart';
@RestApi(baseUrl: "//5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/")
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
@GET("/tasks/{id}")
Future<Task> getTask(@Path("id") String id);
@GET('/demo')
Future<String> queries(@Queries() Map<String, dynamic> queries);
@GET("//httpbin.org/get")
Future<String> namedExample(@Query("apikey") String apiKey, @Query("scope") String scope, @Query("type") String type, @Query("from") int from);
@PATCH("/tasks/{id}")
Future<Task> updateTaskPart(@Path() String id, @Body() Map<String, dynamic> map);
@PUT("/tasks/{id}")
Future<Task> updateTask(@Path() String id, @Body() Task task);
@DELETE("/tasks/{id}")
Future<void> deleteTask(@Path() String id);
@POST("/tasks")
Future<Task> createTask(@Body() Task task);
@POST("//httpbin.org/post")
Future<void> createNewTaskFromFile(@Part() File file);
@POST("//httpbin.org/post")
@FormUrlEncoded()
Future<String> postUrlEncodedFormData(@Field() String hello);
}
@JsonSerializable()
class Task {
String id;
String name;
String avatar;
String createdAt;
Task({this.id, this.name, this.avatar, this.createdAt});
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
Map<String, dynamic> toJson() => _$TaskToJson(this);
}
import 'package:logger/logger.dart';
import 'package:retrofit_example/example.dart';
import 'package:dio/dio.dart';
final logger = Logger();
void main(List<String> args) {
final dio = Dio(); // Provide a dio instance
dio.options.headers["Demo-Header"] = "demo header";
final client = RestClient(dio);
client.getTasks().then((it) => logger.i(it));
}
As a diagram, we will take what the offers. Next, we need to install the generator itself. is a great tutorial for that. If you are reading this article, then with a high degree of probability you already have installed, which means that one of the easiest installation methods would be to use -versions.
openapi-generator-cli generate -i //petstore.swagger.io/v2/swagger.json -g dart-dio -o .pet_api --additional-properties pubName=pet_api
An alternative way is to describe the parameters in the openapitools.json
file, for example:
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "5.1.1",
"generators": {
"pet": {
"input-spec": "//petstore.swagger.io/v2/swagger.json",
"generator-name": "dart-dio",
"output": ".pet_api",
"additionalProperties": {
"pubName": "pet_api"
}
}
}
}
}
openapi-generator-cli generate
# <generator-name>, dart-dio - for example
openapi-generator-cli config-help -g dart-dio
Even if you choose the complete console option, after the first start of the generator, you will have a configuration file with the version of the generator used written in it, as in this example - 5.1.1
. In the case of Dart / Flutter, this version is very important, since each of them can carry certain changes, including those with backward incompatibility or interesting effects.
So, since version 5.1.0
, the generator uses null-safety, but implements this through explicit checks, and not the capabilities of the Dart language itself (for now, unfortunately). For example, if in your schema some of the model fields are marked as required, then if your backend returns a model without this field, then an error will occur in runtime.
flutter: Deserializing '[id, 9, category, {id: 0, name: cats}, photoUrls, [string], tags, [{id: 0, na...' to 'Pet' failed due to: Tried to construct class "Pet" with null field "name". This is forbidden; to allow it, mark "name" with @nullable.
And all because the name
field of the Pet
model is explicitly specified as required, but is absent in the request response:
{
"Pet": {
"type": "object",
"required": [
"name", // <- required field
"photoUrls" // <- and this too
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"category": {
"$ref": "#/definitions/Category"
},
"name": {
"type": "string",
"example": "doggie"
},
"photoUrls": {
"type": "array",
"xml": {
"wrapped": true
},
"items": {
"type": "string",
"xml": {
"name": "photoUrl"
}
}
},
"tags": {
"type": "array",
"xml": {
"wrapped": true
},
"items": {
"xml": {
"name": "tag"
},
"$ref": "#/definitions/Tag"
}
},
"status": {
"type": "string",
"description": "pet status in the store",
"enum": [
"available",
"pending",
"sold"
]
}
},
"xml": {
"name": "Pet"
}
},
"Category": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Category"
}
},
"Tag": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Tag"
}
}
}
Well, the generator is running - the job is done, but there are a few simple steps left in which we hardly have to write code (for the sake of this, everything was started!). The standard openapi-generator
will generate only the basic code, which uses libraries that already rely on code generation by means of Dart itself. Therefore, after completing the basic generation, you need to start Dart / Flutter:
cd .pet_api
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
At the output, we get a ready-made package, which will be located where you specified in the configuration file or console command. It remains to include it in pubspec.yaml
:
name: openapi_sample
description: Sample for OpenAPI
version: 1.0.0
publish_to: none
environment:
flutter: ">=2.0.0"
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
pet_api: # <- our generated library
path: .pet_api
import 'package:dio/dio.dart';
import 'package:pet_api/api/pet_api.dart';
import 'package:pet_api/model/pet.dart';
import 'package:pet_api/serializers.dart'; // <- we must use [standartSerializers] from this package module
Future<Pet> loadPet() async {
final Dio dio = Dio(BaseOptions(baseUrl: '//petstore.swagger.io/v2'));
final PetApi petApi = PetApi(dio, standardSerializers);
const petId = 9;
final Response<Pet> response = await petApi.getPetById(petId, headers: <String, String>{'Authorization': 'Bearer special-key'});
return response.data;
}
An important part of it is the need to write down which serializers we will use in order for JSONs to turn into normal models. And also drop the Dio
instances into the generated ... Api
, specifying the base server URLs in them.
It seems that this is all that can be said on this topic, but Dart recently received a major update, it added null-safety. And all packages are being actively updated, and projects are migrating to a new version of the language, more resistant to our errors.
Language version in the package (in the latest version of the generator - 5.1.1
, Dart 2.7.0
is used)
Backward incompatibility of some of the packages used (in the current version of Dio
, some methods have different names)
name: pet_api
version: 1.0.0
description: OpenAPI API client
environment:
sdk: '>=2.7.0 <3.0.0' # -> '>=2.12.0 <3.0.0'
dependencies:
dio: '^3.0.9' # Actual -> 4.0.0
built_value: '>=7.1.0 <8.0.0' # -> 8.1.0
built_collection: '>=4.3.2 <5.0.0' # -> 5.1.0
dev_dependencies:
built_value_generator: '>=7.1.0 <8.0.0' # -> 8.1.0
build_runner: any # -> 2.0.5
test: '>=1.3.0 <1.16.0' # -> 1.17.9
The second disadvantage is that this generated API package is now a legacy dependency, which will prevent your new project from starting with sound-null-safety
. You will be able to take advantage of null-safety
when writing code, but you will not be able to check and optimize runtime, and the project will only work if you use the additional Flutter parameter: --no-sound-null-safety
.
sound-null-safety
openapi-generator-cli generate
cd .pet_api || exit
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
openapi-generator-cli generate
cd .pet_api || exit
echo "name: pet_api
version: 1.0.0
description: OpenAPI API client
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
dio: ^4.0.0 built_value: ^8.1.0 built_collection: ^5.1.0
dev_dependencies:
built_value_generator: ^8.1.0 build_runner: ^2.0.5 test: ^1.17.9" > pubspec.yaml
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
Now - our generator will start correctly with the new version of Dart (>2.12.0
) in the system. Everything would be fine, but we still won't be able to use our api package! First, the generated code is replete with annotations that bind it to the old version of the language:
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.7 <--
// ignore_for_file: unused_import
const fs = require('fs');
const p = require('path');
const dartFiles = [];
function main() {
const openapiDirPath = p.resolve(__dirname, '.pet_api');
searchDartFiles(openapiDirPath);
for (const filePath of dartFiles) {
fixFile(filePath);
console.log('Fixed file:', filePath);
}
}
function searchDartFiles(path) {
const isDir = fs.lstatSync(path).isDirectory();
if (isDir) {
const dirContent = fs.readdirSync(path);
for (const dirContentPath of dirContent) {
const fullPath = p.resolve(path, dirContentPath);
searchDartFiles(fullPath);
}
} else {
if (path.includes('.dart')) {
dartFiles.push(path);
}
}
}
function fixFile(path) {
const fileContent = fs.readFileSync(path).toString();
const fixedContent = fixOthers(fileContent);
fs.writeFileSync(path, fixedContent);
}
const fixOthers = fileContent => {
let content = fileContent;
for (const entry of otherFixers.entries()) {
content = content.replace(entry[0], entry[1]);
}
return content;
};
const otherFixers = new Map([
// ? Base fixers for Dio and standard params
[
'// @dart=2.7',
'// ',
],
[
/response\.request/gm,
'response.requestOptions',
],
[
/request: /gm,
'requestOptions: ',
],
[
/Iterable<Object> serialized/gm,
'Iterable<Object?> serialized',
],
[
/(?<type>^ +Uint8List)(?<value> file,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +String)(?<value> additionalMetadata,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +ProgressCallback)(?<value> onReceiveProgress,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +ProgressCallback)(?<value> onSendProgress,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +ValidateStatus)(?<value> validateStatus,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +Map<String, dynamic>)(?<value> extra,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +Map<String, dynamic>)(?<value> headers,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +CancelToken)(?<value> cancelToken,)/gm,
'$<type>?$<value>',
],
[
/(@nullable\n)(?<annotation>^ +@.*\n)(?<type>.*)(?<getter> get )(?<variable>.*\n)/gm,
'$<annotation>$<spaces>$<type>?$<getter>$<variable>',
],
[
'final result = <Object>[];',
'final result = <Object?>[];',
],
[
'Iterable<Object> serialize',
'Iterable<Object?> serialize',
],
[
/^ *final _response = await _dio.request<dynamic>\(\n +_request\.path,\n +data: _bodyData,\n +options: _request,\n +\);/gm,
`_request.data = _bodyData;
final _response = await _dio.fetch<dynamic>(_request);
`,
],
// ? Special, custom params for concrete API
[
/(?<type>^ +String)(?<value> apiKey,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +String)(?<value> name,)/gm,
'$<type>?$<value>',
],
[
/(?<type>^ +String)(?<value> status,)/gm,
'$<type>?$<value>',
],
]);
main();
The approach to generating client code, in the presence of a high-quality OpenAPI scheme, is an extremely simple task, regardless of the client's language. In the case of Dart, there are still certain inconveniences caused, especially, by the transition period to * null-safety *. But as part of this article, we have successfully overcome all the troubles and got a fully functional library for working with the backend, the dependencies of which (and itself) have been updated to the newest version and can be used in a Flutter project with sound-null-safety without any restrictions.