Managing common response types with reflection in dart

Furkan Acar
6 min readJan 29, 2024

--

In this article, I won’t be examining reflection from start to finish as there are already good articles that do this. Instead, after a brief introduction, I will try to explain how we can integrate reflection with response types.

Generated by Gpt

What is reflection?

In Dart, reflection is a feature that allows programs to examine and modify their own structure and behavior at runtime. The dart:mirrors library supports this capability by providing information on classes, functions, and libraries. Dart's reflection system is harmonized with its dynamic typing, enabling developers to manipulate types more flexibly during runtime. The concept of mirrors in Dart's reflection mechanism provides access to information about objects, including their types, members, and values.

Why did I need to use reflection?

In my case, there was a base response structure coming from API calls. The base part of this structure consists of two different fields: message and data. The message field can be just a string, but the data field can be a simple model, it can be a list containing only one type of model, but there can be lists of different types.

// BaseType 1
{
"message": "Success",
"data": "data"
}
// BaseType 2
{
"message": "string",
"data": {
"dif_data": "dif_data",
"dif_no": 1,
"dif_something": "something"
}
}

//BaseType 3
{
"message": "Success",
"data": [
{
"data": "data",
"no": 1,
"something": "something"
},
{
"data": "data",
"no": 1,
"something": "something"
}
]
}
//BaseType 4
{
"message": "Success",
"data": [
{
"dif_data_two": "data",
"dif_no_two": 1,
"dif_something_two": "something"
},
{
"dif_data_two": "data 2",
"dif_no_two": 2,
"dif_something_two": "something 2"
},
]
}

//.....

Base Type 1: Data can be a string

Base Type 2: Data can be a model

Base Type 3: Data can be a list

Base Type 4: Data can be a list with different model

As we see here, message will be a string, and data may have different models.

It seems like we can solve this with generic structures. let’s try

As you see “The method ‘fromJson’ isn’t defined for the type ‘Type’.” We get the error because dart does not know whether the T type has a fromJson method or not.

This happens because t does not know our model at the moment, but even if we extend T from ModelOne, nothing will change.

In Dart’s type system, calling a method on a generic type Trequires determining which concrete class this type corresponds to at compile time, leading to certain complexities. The expression 'Textends ModelOne’ indicates that T will be a class derived from ‘ModelOne’, but it is unknown which specific class Twill correspond to at compile time. Therefore, the Dart compiler cannot directly call the ‘T.fromJson’ method.

You can solve this problem in two different ways.

First and easy, you can give the FromJson of the model you want to convert to the fromJson of the BaseResponse model.

class BaseResponse<T extends ModelOne> {
final String? message;
final T? data;

BaseResponse(this.message, this.data);

factory BaseResponse.fromJson(Map<String, dynamic> json, T Function(Map<String, dynamic>) fromJsonT) {
return BaseResponse(
json["message"] as String?,
json["data"] != null ? fromJsonT(json["data"]) : null,
);
}
}

You can use it like this;

BaseResponse<ModelOne>.fromJson(jsonData, ModelOne.fromJson);

But if you are using an HTTP client generator for Dart such as retrofit or chopper, this will not be a valid solution for you. You can still solve it this way, but it will get a little complicated.

So what can we do?

Reflection may be the solution to this problem.

Using reflection, we find the types of the incoming models and call their fromJson methods.

If you want to get introductory information about reflection, you can look at this article. I will start explaining directly with an example.

First, we add reflectable and build_runner dependencies.

name: reflection_example
description: "A new Flutter project."
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
reflectable: ^4.0.5

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
build_runner: ^2.4.8
flutter:
uses-material-design: true

After this, we create a class that implements the Reflectable class with the name you want. We will use this class as an annotation. and we will write the reflection features we want to use in super..

import 'package:reflectable/reflectable.dart';

class Reflector extends Reflectable {
const Reflector()
: super(
newInstanceCapability,
instanceInvokeCapability,
);
}

const reflector = Reflector();

You can use reflection features by adding the @reflactor annotation to the classes you want to add reflection features.

import 'package:reflection_example/reflactor/Reflector.dart';

@reflector
class ModelOne {
String? difData;
int? difNo;
String? difSomething;

ModelOne({this.difData, this.difNo, this.difSomething});

ModelOne.fromJson(Map<String, dynamic> json) {
difData = json['dif_data'];
difNo = json['dif_no'];
difSomething = json['dif_something'];
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['dif_data'] = difData;
data['dif_no'] = difNo;
data['dif_something'] = difSomething;
return data;
}
}

Now we can find the ModelOne class type at run time and create instances from it using reflection features or use other features.

mport 'dart:developer';
import 'package:reflectable/reflectable.dart';
import 'package:reflection_example/reflactor/Reflector.dart';


class BaseResponse<T> {
final String? message;
final T? data;

BaseResponse(this.message, this.data);

factory BaseResponse.fromJson(Map<String, dynamic> json) {
var data;
var message;

try {
message = json["message"] as String;
if (T == String) {
data = json["data"] as String;
} else {
var classMirror = reflector.reflectType(T) as ClassMirror;
if (json['data'] != null && json['data'] is Map) {
data = classMirror.newInstance(
"fromJson", [(json['data']) as Map<String, dynamic>]) as T;
}
}
} catch (e) {
log(e.toString());
}

return BaseResponse(
message,
data,
);
}
}

For example, if the T type is a string, we try to get the data directly as a string, but if the T type is another Model, we create an instance with its constructor created with fromJson, such as ‘ModelOne’, and assign it to the data.

For example, if our data is a list, we can handle it here, but I preferred to show another model.

import 'dart:developer';
import 'package:reflectable/reflectable.dart';
import 'package:reflection_example/reflactor/Reflector.dart';

class BaseResponseList<T> {
final String? message;
final List<T>? data;
BaseResponseList(this.message, this.data);
factory BaseResponseList.fromJson(Map<String, dynamic> json) {
List<T>? data;
String? message;
try {
message = json["message"] as String?;
var classMirror = reflector.reflectType(T) as ClassMirror;
if (json['data'] != null && json['data'] is List) {
data = (json['data'] as List).map((e) {
return classMirror
.newInstance("fromJson", [(e) as Map<String, dynamic>]) as T;
}).toList();
}
} catch (e) {
log(e.toString());
}
return BaseResponseList(
message,
data,
);
}
}

In order to perform the generation process, we need to create the build.yamlfile. If you are going to use it as I gave you, you need to put it in the same directory as the pubspecfile. If you want to put it in a different module, add the path of that directory to the generator for field.

targets:
$default:
builders:
reflectable:
generate_for:
- lib/main.dart
options:
formatted: true

After adding @reflactor annotation to the classes to which you want to add the reflection feature, you can perform the generation process by running the dart run build_runner build — delete-conflicting-outputs command and use the reflection features for these classes.

Pay particular attention to the generate_forfield. If you are using another module, you need to add the path of that module there, otherwise the generator will not work.

To keep this article simple and short, I do not show the retrofit and chopper connection processes. If you follow the steps here correctly, you will not have to do anything different with HTTP generators. But if there is demand, you can contact me and we can examine it in a new article.

Thank you for reading :) I hope you like it. If you have any questions, you can reach me through social media :) @furkanacardev

Source Code:

--

--