Avoid writing repetitive code for API integration in Flutter/dart.

Ahadu Sefefe
7 min readJun 26, 2023

Rest API integration is arguably the most implemented feature next to UI implementation in Mobile/Frontend Development. When working on this feature, the backend team provides documentation, telling what each request/response looks like. And one way this gets done is with Postman.

On my last Flutter app development project, I was given a big Postman collection to follow and integrate the backend to the mobile app I was building. Thankfully the collection was well documented but the app was big so there were lots of endpoints to communicate with.

I was all ready and up to start coding the models, services, and classes I need until I see the collection one more time and it hits me. Postman collection is just JSON data. A map with well-organized key-value pairs where everything is a repetition of a certain structure with few variable data.

When working on API integration, we create 3 essential parts; A service that handles the connection and everything related separately, classes that inject the service and send the requests, and lastly, models that act as blueprints to the data sent/received when communicating.

In a nicely written code base, the first one is a set of classes that are written once and will handle connecting with the backend, sending requests, handling connection-related errors, and functionalities of our like.

This is a method to show the idea. It is written once but all requests pass through and it works with other methods/classes to process the integration.

The latter two on the contrary are different for every request, but with so many repetitive code blocks and structures.

For a single request made, we create a method with properties like paths, request type, and request body( if needed), that depends on the request itself. Then a common implementation for all requests is sending them via an injected instance of the first service. And this process is repeated for every API request.

Register, login, and verify OTP; all requests share the same format but differ in properties

One other thing to point out here is, we should always put related requests/methods together. All the above methods for instance can be put in a class called Auth. I also create an abstract class that holds a blueprint for what a corresponding class contains (like an interface).

A class containing related request methods and implementing an interface for code clarity.

And lastly, the models: classes representing request/response bodies that we create too many times, but all have the same format.

Model classes with a similar format but with their own variables, of course.

Now enough with the boring part. What does a Postman collection hold that we can use to create these repetitive codes?

On a well-documented Postman collection, each related request is stored in a folder, which from the above points will become a dart class in our implementation. And every property we mentioned above that defines what each request need can be found here.

A Postman collection and what its JSON format looks like.

Now the general idea is if we have an implementation that is mostly similar and repetitive JSON data, we can use this behavior to iterate through the JSON data(map), and together with a template of each part we can write a script that will create every model, request functions and binding classes, and helper files of personal preferences.

So simply I wrote a dart code that reads the JSON file, iterates through the map, creates separate folders to compile with the given structure, creates a class file for every ‘folder’, and uses templates(check the image below) to write the class details, write the functions for every request, and models for the request bodies.

Note that I use dio as HTTP client and handle API integration, and freezed for serialization. So switch up the templates to what you desire.

Now we iterate through each “folder” and create methods for each, create the repository class, enter the methods, and write them on a file. Writing the adapter class goes together within each loop. As for the models, we collect them all together when found and save those to a file altogether.

void startBuild({required Map<String, dynamic> repoJson}) {
repoJson['item'].forEach((folder) {
// for every 'folder' create methods, then repositories, then adapter.
_currentRepoJson = folder;
_createMethods(json: _currentRepoJson['item']);
_createRepository();
_createAdapter();
});

// once all the models in the collection are gathered save them to a file
_saveModels();
}
void _createMethods({required List<dynamic> json}) {
// empty strings to hold methods. repo methods is for methods with their implementations written as
// repositories expect that. adapter methods are only function definitions since adapters expect that.
String repoMethods = "", adapterMethods = "";

// create methods: if the current folder have child folder inside it: data[item] will be List<dynamic>
// as it will contain list of folder, else there will be data[request] instead, which is the method.
json.forEach((data) {
if (data['item'] is List<dynamic>) {
_createMethods(json: data['item']);
} else {
MethodModel model = MethodModel.fromJson(data);
// if current request has payload that is put as json, create model for that request payload
if (model.hasPayload && data['request']?['body']?['mode'] == 'raw') {
_createModel(
name: data['name'], body: data['request']['body']['raw']);
}

repoMethods += repoMethodTemplate(data: model) + "\n";
adapterMethods += adapterMethodTemplate(model: model) + "\n";
}
});
_methods = {repoMethods, adapterMethods};
}

// create repository file based on current class and write string from template together with methods
void _createRepository() {
File repo = FileService.createFile(
fileName: _currentRepoJson['name'], classType: ClassType.repository);
String repoTemplate = repositoryTemplate(
repoName: _currentRepoJson['name'], functions: _methods.first);
repo.writeAsString(repoTemplate);
}

// create adapter file based on current class and write string from template together with methods
void _createAdapter() {
File adapter = FileService.createFile(
fileName: _currentRepoJson['name'], classType: ClassType.adapter);
String adapTemplate = adapterTemplate(
repoName: _currentRepoJson['name'], methods: _methods.last);
adapter.writeAsString(adapTemplate);
}

// generate all variables needed in a single model and create the models with the template stored
_createModel({
required String name,
required String body,
}) {
try{
String variables = _getModelVariable(body: jsonDecode(body));
models += modelMethodTemplate(name: name.toClassName, variables: variables);
} catch (e) {
print(name+"\n"+body.runtimeType.toString());
print(e);
}
}

And done. Once we feed our program with the collection and run it, we have all the code we need, written on the respective folder.

Lastly, for the models - only those from the request bodies can be found from this. What the response bodies look like can only be known after the back end sends the responses. No worries though, I have a JS version of this code below. Copy that, go to Postman and on your collection’s main folder paste it to ‘Tests’ and it will give you the models every time you got a response.

if(pm.response.code==200)
{
const responseJson = pm.response.json();
if(responseJson.data){
var data=responseJson.data;
console.log(modelTemplate(toClassName(this.request.name),data));
}
}

function modelTemplate(modelName,data) {
let dataTypes = "";
let extras = "";
for(var key in data){
let value=data[key]
let type=typeof(value);
if (type == undefined)
;
else if (type ==
"object"){

if(value==null){
dataTypes = dataTypes + `dynamic ${key},\n`;
}
else if(!Array.isArray(value)) {
extras = extras +
modelTemplate(`${toClassName(key)}Model`, value);
dataTypes = dataTypes + `${toClassName(key)}Model? ${key},\n`;
}
else{
dataTypes = dataTypes + `List<dynamic>? ${key},\n`;
}
}else {
dataTypes = dataTypes + `${getDartClass(type)}? ${key},\n`;
}
};
return `@freezed\nabstract class ${modelName} with _\$${modelName} {\nfactory ${modelName}({\n${dataTypes}\n}) = _${modelName};\nfactory ${modelName}.fromJson(Map<String, dynamic> json) => _\$${modelName}FromJson(json);\n}\n${extras}\n`;
}

function toClassName(name) {
var temp = name.split(/[\s_]/);
var fileName = "";
temp.forEach((element)=> {
fileName = fileName + element[0].toUpperCase() + element.substring(1);
});
return fileName;
}

function getDartClass(name){
switch (name){
case "string":
return "String";
case "number":
return "int";
case "boolean":
return "bool";
case "":
return "String";
}
}
Note that checking code status and for response body’s ‘data’ parameter are commented on the picture for this particular case but you can edit it as your interest.

That is all. Thank you for reading and I hope this was helpful. You can find the code on GitHub: https://github.com/Ahadu-ray/flutter_restapi_integration_starter_files_generator
Let’s connect on LinkedIn https://www.linkedin.com/in/ahadu-sefefe/ or say hi via my email ahadusefefe.123@gmail.com.

--

--