Few days ago, we’ve had to serialize a paged results response.
This PagedResult object has a List of Generic results, so depending on what result we want to receive, we need to map as one or othe Class.
The Trouble
Flutter doesn’t supports Reflection and neither allow to pass class or constructor as function parameter.
Model examples
class PagedResult<T> {
final int totalItems;
final int startItems;
final int itemsPerPage;
final int currentPage;
final int totalPages;
final List<T> results;
PagedResult({
this.totalItems,
this.startItems,
this.itemsPerPage,
this.currentPage,
this.totalPages,
this.results});
}
@JsonSerializable()
class Customer {
final int id;
final String firstName;
final String lastName;
final String secondLastName;
final String phone1;
final String phone2;
final String email;
Customer(
{this.id,
this.firstName,
this.lastName,
this.secondLastName,
this.phone1,
this.phone2,
this.email});
factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
@JsonSerializable()
class Product {
final int id;
final String ref;
final double price;
final String description;
final double tax;
Product(
{this.id,
this.ref,
this.price,
this.description,
this.tax});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
Our goal is to serialize PagedResults as PagedResults of Customer and PagedResults of Product.
We have multiple options to do it:
Brute force
We can create one PagedResult class for each "class":
class PagedResultCustomer<Customer> {
final int totalItems;
final int startItems;
final int itemsPerPage;
final int currentPage;
final int totalPages;
final List<Customer> results;
PagedResult({
this.totalItems,
this.startItems,
this.itemsPerPage,
this.currentPage,
this.totalPages,
this.results});
factory PagedResultCustomer.fromJson(Map<String, dynamic> json) {
final items = json['results'].cast<Map<String, dynamic>>();
return PagedResult<Customer>(
totalItems = json.totalItems;
.
.
.
results = new List<Customer>.from(items.map((itemsJson) => Customer.fromJson(itemsJson)))
)
}
}
class PagedResultProduct<Product> {
final int totalItems;
final int startItems;
final int itemsPerPage;
final int currentPage;
final int totalPages;
final List<Product> results;
PagedResult({
this.totalItems,
this.startItems,
this.itemsPerPage,
this.currentPage,
this.totalPages,
this.results});
factory PagedResultProduct.fromJson(Map<String, dynamic> json) {
final items = json['results'].cast<Map<String, dynamic>>();
return PagedResult<Product>(
totalItems = json.totalItems;
.
.
.
results = new List<Product>.from(items.map((itemsJson) => Product.fromJson(itemsJson)))
)
}
It's a simple solution but not accomplish with DRY PRINCIPLE
Young Padawan
Another way is try to follow DRY PRINCIPLE and based on code above we can do:
class PagedResult<T> {
final int totalItems;
final int startItems;
final int itemsPerPage;
final int currentPage;
final int totalPages;
final List<T> results;
PagedResult({
this.totalItems,
this.startItems,
this.itemsPerPage,
this.currentPage,
this.totalPages,
this.results});
/// We require a second parameter to be an object that implements a
/// fromJson method. Ok, I'm not using Typing for this, but if object
/// hasn't fromJson method, we'll throw a new Exception :)
factory PagedResult.fromJson(Map<String, dynamic> json, object) {
final items = json['results'].cast<Map<String, dynamic>>();
return PagedResult<T>(
totalItems = json.totalItems;
.
.
.
results = new List<T>.from(items.map((itemsJson) => object.fromJson(itemsJson)))
)
}
}
@JsonSerializable()
class Customer {
final int id;
final String firstName;
final String lastName;
final String secondLastName;
final String phone1;
final String phone2;
final String email;
Customer(
{this.id,
this.firstName,
this.lastName,
this.secondLastName,
this.phone1,
this.phone2,
this.email});
/// We don't use factory, instead we implement a fromJson method object
Customer fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
To run:
/// We pass a new object Customer as parameter
final PagedResult<Customer> pagedCustomer = PagedResult<Customer>.fromJson(json, new Customer());
Ok, it's a better solution than Brute Force but I don't like the requirement to need to instantiate a new object for this.
Jedi
Dart doesn't support Reflection, or pass class/factory as parameter, but it allow to pass a method :)
So we can change a bit the code above:
class PagedResult<T> {
final int totalItems;
final int startItems;
final int itemsPerPage;
final int currentPage;
final int totalPages;
final List<T> results;
PagedResult({
this.totalItems,
this.startItems,
this.itemsPerPage,
this.currentPage,
this.totalPages,
this.results});
/// We require a second parameter to be a Function, and we'll trigger
/// this function for serializing results property
factory PagedResult.fromJson(Map<String, dynamic> json, Function fromJson) {
final items = json['results'].cast<Map<String, dynamic>>();
return PagedResult<T>(
totalItems = json.totalItems;
.
.
.
results = new List<T>.from(items.map((itemsJson) => fromJson(itemsJson)))
)
}
}
@JsonSerializable()
class Customer {
final int id;
final String firstName;
final String lastName;
final String secondLastName;
final String phone1;
final String phone2;
final String email;
Customer(
{this.id,
this.firstName,
this.lastName,
this.secondLastName,
this.phone1,
this.phone2,
this.email});
/// We change object method to a static class method (avoid instantiate object)
static Customer fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
To run:
/// We pass a new object Customer as parameter
final PagedResult<Customer> pagedCustomer = PagedResult<Customer>.fromJson(json, Customer.fromJson);
Of course, there are more solutions to serialize generics but I think the Jedi method is a fine and clear solution to do it.
Top comments (5)
You are the man! I'm figthing this freaking generic thingy for an entire day, glad I found your solution. I was very close, the function parameter did the trick, can't go with a 100% clean solution, seems the Jedi is the way to go with Dart. Thanks!
It seems Jedi method is best so far, you save my day man, thank you very much
Awesome! Thank you
What about nested generics ? If there is a base response class that wraps PagedResult like
class BaseResponse {
bool succeeded;
String message;
PagedResult innerData;
}
You can do something like this:
An as the last example with Customer:
Of course the "json" structure depends on your http response.