6

My app currently is working with a custom classes for each API responses as models. But I'm trying to change it, to optimize some little things, so I'm trying to implement a Class wrapper, called ApiResponse for example. But its not working fine the static call and methods, for make fromJson and toJson.

I will show what I'm trying, as example.

MyModel -> class response.
ApiResponse -> main class that contains any model class inside, and must be call child methods as itselfs 'fromjson/tojson'.
Test -> class for test purpose, errors comments on classes.

class MyModel {
  String id;
  String title;
  MyModel({this.id, this.title});

  factory MyModel.fromJson(Map<String, dynamic> json) {
    return MyModel(
      id: json["id"],
      title: json["title"],
    );
  }

  Map<String, dynamic> toJson() => {
        "id": this.id,
        "title": this.title,
      };
}

class ApiResponse<T> {
  bool status;
  String message;
  T data;
  ApiResponse({this.status, this.message, this.data});

  factory ApiResponse.fromJson(Map<String, dynamic> json) {
    return ApiResponse<T>(
        status: json["status"],
        message: json["message"],
        data: (T).fromJson(json["data"])); // The method 'fromJson' isn't defined for the type 'Type'.
                                           // Try correcting the name to the name of an existing method, or defining a method named 'fromJson'.
  }

  Map<String, dynamic> toJson() => {
        "status": this.status,
        "message": this.message,
        "data": this.data.toJson(), // The method 'toJson' isn't defined for the type 'Object'.
                                    // Try correcting the name to the name of an existing method, or defining a method named 'toJson'
      };
}

class Test {
  test() {
    ApiResponse apiResponse = ApiResponse<MyModel>();
    var json = apiResponse.toJson();
    var response = ApiResponse<MyModel>.fromJson(json);
  }
}

5 Answers 5

11

You can't call methods on types on Dart because static methods must be resolved at compile time and types do not have a value until runtime.

You can, however, pass a parser callback to your constructor and use an interface(eg. Serializable) that every model may implement. Then, by updating your ApiResponse to ApiResponse<T extends Serializable> it will know that every type T will have a toJson() method.

Here's the full example updated.

class MyModel implements Serializable {
  String id;
  String title;
  MyModel({this.id, this.title});

  factory MyModel.fromJson(Map<String, dynamic> json) {
    return MyModel(
      id: json["id"],
      title: json["title"],
    );
  }

  @override
  Map<String, dynamic> toJson() => {
        "id": this.id,
        "title": this.title,
      };
}

class ApiResponse<T extends Serializable> {
  bool status;
  String message;
  T data;
  ApiResponse({this.status, this.message, this.data});

  factory ApiResponse.fromJson(Map<String, dynamic> json, Function(Map<String, dynamic>) create) {
      return ApiResponse<T>(
      status: json["status"],
      message: json["message"],
      data: create(json["data"]),
    );
  }

  Map<String, dynamic> toJson() => {
        "status": this.status,
        "message": this.message,
        "data": this.data.toJson(),
      };
}

abstract class Serializable {
  Map<String, dynamic> toJson();
}

class Test {
  test() {
    ApiResponse apiResponse = ApiResponse<MyModel>();
    var json = apiResponse.toJson();
    var response = ApiResponse<MyModel>.fromJson(json, (data) => MyModel.fromJson(data));
  }
}
Sign up to request clarification or add additional context in comments.

9 Comments

It seems work, but the problem now is, when im in the controller getting the response, for example, i cant get the data of the model: response.data.title 'The getter 'id' isn't defined for the type 'Serializable'. Try importing the library that defines 'id', correcting the name to the name of an existing getter, or defining a getter or field named 'id''. And this is a problem, because I need the data of the model, i will have many models and properties. What would u do in that case?
You're saying that you can't do response.data.id? You should be able without an issue.
i mean under test response yes, but im working a TDD project flutter, so all this process is inside a remote data source, so this one returns the ApiResponse class, and now in the next level, i just get the response ApiResponse response, for example, so there is now way i cant get response.data.id. Do you understand what im trying to say?
Ok i solved it, using just a cast, doing AnotherModel data = response.data as AnotherModel. And it works, thanks you so much dude. ;)
Im having a problem, what if the response is not and object class "T", and is just a boolean or string, how do you handle it? i got error on "boolean is not serializable" or "string"... thanks
|
9

base_response.dart

class BaseResponse {
  dynamic message;
  bool success;


  BaseResponse(
      {this.message, this.success});

  factory BaseResponse.fromJson(Map<String, dynamic> json) {
    return BaseResponse(
        success: json["success"],
        message: json["message"]);
  }
}

list_response.dart

server response for list
{
  "data": []
  "message": null,
  "success": true,
}

@JsonSerializable(genericArgumentFactories: true)
class ListResponse<T> extends BaseResponse {
  List<T> data;

  ListResponse({
    String message,
    bool success,
    this.data,
  }) : super(message: message, success: success);

  factory ListResponse.fromJson(Map<String, dynamic> json, Function(Map<String, dynamic>) create) {
    var data = List<T>();
    json['data'].forEach((v) {
      data.add(create(v));
    });

    return ListResponse<T>(
        success: json["success"],
        message: json["message"],
        data: data);
  }
}

single_response.dart

server response for single object
{
  "data": {}
  "message": null,
  "success": true,
}


@JsonSerializable(genericArgumentFactories: true)
class SingleResponse<T> extends BaseResponse {
  T data;

  SingleResponse({
    String message,
    bool success,
    this.data,
  }) : super(message: message, success: success);

  factory SingleResponse.fromJson(Map<String, dynamic> json, Function(Map<String, dynamic>) create) {
    return SingleResponse<T>(
        success: json["success"],
        message: json["message"],
        data: create(json["data"]));
  }
}

data_response.dart

class DataResponse<T> {
  Status status;
  T res; //dynamic
  String loadingMessage;
  GeneralError error;

  DataResponse.init() : status = Status.Init;

  DataResponse.loading({this.loadingMessage}) : status = Status.Loading;

  DataResponse.success(this.res) : status = Status.Success;

  DataResponse.error(this.error) : status = Status.Error;


  @override
  String toString() {
    return "Status : $status \n Message : $loadingMessage \n Data : $res";
  }
}

enum Status {
  Init,
  Loading,
  Success,
  Error,
}

or if using freeezed then data_response can be

@freezed
abstract class DataResponse<T> with _$DataResponse<T> {
  const factory DataResponse.init() = Init;
  const factory DataResponse.loading(loadingMessage) = Loading;
  const factory DataResponse.success(T res) = Success<T>;
  const factory DataResponse.error(GeneralError error) = Error;
}

Usage: (part of retrofit library

@GET(yourEndPoint)
Future<SingleResponse<User>> getUser();

@GET(yourEndPoint)
Future<ListResponse<User>> getUserList();

If don't use reftofit:

const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.request<Map<String, dynamic>>('$commentID',
    queryParameters: queryParameters,
    options: RequestOptions(
        method: 'GET',
        headers: <String, dynamic>{},
        extra: _extra,
        baseUrl: baseUrl),
    data: _data);
final value = SingleResponse<Comment>.fromJson(
  _result.data,
  (json) => Comment.fromJson(json),
);

2 Comments

This is a better approach to me. It allows to use the code generator with the flutter retrofit. @JsonSerializable(genericArgumentFactories: true) - This is useful
Thanks @JsonSerializable(genericArgumentFactories: true) helps. Retrofit supports dynamic generic fromJson code generation.
1

You can try my approach to apply generic response: APIResponse<MyModel> by implements a custom Decodable abstract class, response from http requests will return as MyModel object.

Future<User> fetchUser() async {

    final client = APIClient();

    final result = await client.request<APIResponse<User>>(
      manager: APIRoute(APIType.getUser), 
      create: () => APIResponse<User>(create: () => User())
    );

    final user = result.response.data; // reponse.data will map with User

    if (user != null) {
      return user;
    }

    throw ErrorResponse(message: 'User not found');

}

Here is my source code: https://github.com/katafo/flutter-generic-api-response

Comments

0
There is another way, 

Map<String, dynamic> toJson() => {
        "message": message,
        "status": status,
        "data": _toJson<T>(data),
      };

static T _fromJson<T>(Map<String, dynamic> json) {
    return ResponseModel.fromJson(json) as T;
  }

1 Comment

the serialization will give u problems
-1

Use https://pub.dev/packages/json_factory_generator

import 'package:example/generated/json_factory.dart';
import 'package:json_annotation/json_annotation.dart';

part 'base_response.g.dart';

/// Generic API response wrapper with JsonSerializable
@JsonSerializable(genericArgumentFactories: true)
class BaseResponse<T> {
  final bool success;
  final String message;
  @DataConverter()
  final T? data;
  final int? code;

  BaseResponse({
    required this.success,
    required this.message,
    this.data,
    this.code,
  });

  /// Generated fromJson with generic type support
  factory BaseResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Object? json) fromJsonT,
  ) => _$BaseResponseFromJson(json, fromJsonT);

  /// Generated toJson with generic type support
  Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
      _$BaseResponseToJson(this, toJsonT);

  @override
  String toString() {
    return 'BaseResponse(success: $success, message: $message, data: $data, code: $code)';
  }
}

class DataConverter<T> implements JsonConverter<T?, Object?> {
  const DataConverter();

  @override
  T? fromJson(Object? json) {
    return JsonFactory.fromJson(json);
  }

  @override
  Object? toJson(T? object) {
    return object;
  }
}

1 Comment

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.