Is there any way to include factory method into abstract class? I've got a lot of model classes which I use to populate data. They all have same methods in them fromMap and toMap where fromMap is an factory method
class MyModelClass implements Tools {
//toMap
Map<String, dynamic> toMap() {
return {
...
};
}
//fromMap
factory MyModelClass.fromMap(
Map<String, dynamic> data) {
...
return MyModelClass(...)}
}
I can easily include toMap method but trouble with factory one
abstract class Tools {
Map<String, dynamic> toMap();
///here the factory method
}
Reason to put these methods in to abstract class is to be able to use the abstract class only in my database class to retrieve and send data.
An idea what comes to my mind
abstract class Tools {
Map<String, dynamic> toMap();
factory Tools.fromMap(Map<String, dynamic> data, String sw) {
if (sw == 'a')
return MyModelClass1();
else
return MyModelClass2();
}
}
Your factory constructor must return an instance of MyModelClass, but that's abstract and cannot be instantiated. It can instantiate and return a (possibly private) concrete, derived class. That is what Map's constructor does. (Map is an abstract class with factory constructors.)
Related
I have a class which I am trying to use with Freezed, Json Serializable, and Hive. After running dart run build_runner build and generating the necessary classes, my compiler gives me the following error:
: Error: Can't use '_$FooBarFromJson' because it is declared more than once.
and
: Error: '_$FooBarFromJson' is already declared in this scope.
part 'foobar.freezed.dart';
part 'foobar.g.dart';
#freezed
#JsonSerializable(explicitToJson: true)
#HiveType(typeId: 0)
class FooBar extends HiveObject with _$FooBar {
factory FooBar({
#HiveField(0) required int baz
}) = _FooBar;
factory FooBar.fromJson(Map<String, dynamic> json) =>
_$FooBarFromJson(json);
}
}
After looking through the generated classes, my foobar.g.dart file contains the following methods:
FooBar _$FooBarFromJson(Map<String, dynamic> json) => FooBar(
baz: json['baz'] as int,
);
Map<String, dynamic> _$FooBarToJson(FooBar instance) =>
<String, dynamic>{
'baz': instance.baz,
};
_$_FooBar _$$_FooBarFromJson(Map<String, dynamic> json) =>
_$_FooBar(
baz: json['baz'] as int,
);
Map<String, dynamic> _$$_FooBarToJson(_$_FooBar instance) =>
<String, dynamic>{
'baz': instance.baz,
};
And my foobar.freezed.dart contains this method:
FooBar _$FooBarFromJson(Map<String, dynamic> json) {
return _FooBar.fromJson(json);
}
I've noticed that other files that get converted only have the methods with the _$$_ prefix in foobar.g.dart, whereas _$FooBarFromJson is being created in both foobar.freezed.dart and foobar.g.dart, which is the cause of the errors. What am I missing here?
According to the following this comment in a freezed issue and as shown in the package readme example, you need to place the #JsonSerializable(explicitToJson: true) inside the class in the following manner:
part 'foobar.freezed.dart';
part 'foobar.g.dart';
#freezed
#HiveType(typeId: 0)
class FooBar extends HiveObject with _$FooBar {
#JsonSerializable(explicitToJson: true) // <~~~ here
factory FooBar({
#HiveField(0) required int baz
}) = _FooBar;
factory FooBar.fromJson(Map<String, dynamic> json) =>
_$FooBarFromJson(json);
}
}
You will notice this will give you the following warning:
The annotation 'JsonSerializable' can only be used on classes
This is a known issue/limitation and the maintainer recommends disabling that specific warning as discussed here.
I believe an alternative approach is to create build.yaml and specify explicitToJson in there but I don't have much knowledge there.
I'm using the freezed package to work with immutable models and make use of the built-in feature for json serialization by the json_serializable package. I have a simple User class/model with different union types (UserLoggedIn, UserGeneral, UserError):
#freezed
class User with _$User {
const factory User(String id, String email, String displayName) =
UserLoggedIn;
const factory User.general(String email, String displayName) = UserGeneral;
const factory User.error(String message) = UserError;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Since I'm using multiple constructors and don't want my API to include the runtimeType key as suggested by the documentation, I can write a converter (scroll a bit more down, sentence starts with: If you don't control the JSON response, then you can implement a custom converter.).
So based on that I wrote the following converter class:
class UserConverter implements JsonConverter<User, Map<String, dynamic>> {
const UserConverter();
#override
User fromJson(Map<String, dynamic> json) {
if (json['id'] != null) {
return UserLoggedIn.fromJson(json);
} else if (json['error'] != null) {
return UserError.fromJson(json);
} else {
return UserGeneral.fromJson(json);
}
}
#override
Map<String, dynamic> toJson(User data) => data.toJson();
}
The documentation now references another class (a wrapper class) which would now use this converter via annotation, something like this:
#freezed
class UserModel with _$UserModel {
const factory UserModel(#UserConverter() User user) = UserModelData;
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
}
Question: is it possible to make use of this converter without having to use a wrapper class (UserModel)?
Reasoning: this wrapper class is adding another layer of abstraction which is not needed (in my cases). Especially since the wrapper class does not have any other benefit / purpose and it feels like it should be possible to do that without using it.
I have freezed model (simplified):
part 'initial_data_model.freezed.dart';
part 'initial_data_model.g.dart';
#freezed
class InitialDataModel with _$InitialDataModel {
const factory InitialDataModel() = Data;
const factory InitialDataModel.loading() = Loading;
const factory InitialDataModel.error([String? message]) = Error;
factory InitialDataModel.fromJson(Map<String, dynamic> json) => _$InitialDataModelFromJson(json);
}
documentation says how to assign custom converters on fields but not on model itself
I got json from backend and somewhere in api_provider I do
return InitialDataModel.fromJson(json);
I have no control on json structure, there aren't "runtimeType" and other stupid redundant things
when I want to create a model from json I call fromJson I'm having this
flutter: CheckedFromJsonException
Could not create `InitialDataModel`.
There is a problem with "runtimeType".
Invalid union type "null"!
ok, again
I have api_provider
final apiProvider = Provider<_ApiProvider>((ref) => _ApiProvider(ref.read));
class _ApiProvider {
final Reader read;
_ApiProvider(this.read);
Future<InitialDataModel> fetchInitialData() async {
final result = await read(repositoryProvider).send('/initial_data');
return result.when(
(json) => InitialDataModel.fromJson(json),
error: (e) => InitialDataModel.error(e),
);
}
}
you may see I'm trying to create InitialDataModel from json
this line throws an error I mentioned above
I don't understand how to create InitialDataModel from json, now in my example it's just empty model, there are no fields
(json) => InitialDataModel.fromJson(json),
json here is Map, it shows an error even if I pass simple empty map {} instead of real json object
The easiest solution is to use the correct constructor instead of _$InitialDataModelFromJson. Example:
#freezed
class InitialDataModel with _$InitialDataModel {
const factory InitialDataModel() = Data;
...
factory InitialDataModel.fromJson(Map<String, dynamic> json) => Data.fromJson(json);
}
The drawback of course is that you can only use the fromJson when you're sure you have the correct json, which isn't great. I actually wouldn't recommend this way because it leaves to the caller the burden of checking the validity and calling the correct constructor.
Another solution, maybe the best, is to follow the documentation and create a custom converter, even though this would require you to have two separated classes.
Otherwise you could chose a different approach and separate the data class from the union, so you'll have a union used just for the state of the request and a data class for the success response:
#freezed
class InitialDataModel with _$InitialDataModel {
factory InitialDataModel(/* here go your attributes */) = Data;
factory InitialDataModel.fromJson(Map<String, dynamic> json) => _$InitialDataModelFromJson(json);
}
#freezed
class Status with _$Status {
const factory Status.success(InitialDataModel model) = Data;
const factory Status.loading() = Loading;
const factory Status.error([String? message]) = Error;
}
and then
[...]
return result.when(
(json) => Status.success(InitialDataModel.fromJson(json)),
error: (e) => Status.error(e),
);
[...]
This question already has an answer here:
Calling method on generic type Dart
(1 answer)
Closed 1 year ago.
I want to make APIHelper class.
This class will have method like get, post, put and delete.
And in these method all logic about getting data, decoding, encoding, mapping will be done.
I have base model class like this:
class Model{
Model();
Model.fromJson(Map<String, dynamic> data);
Map<String, dynamic> toJson(){
return {};
}
}
And in API model Event I inherited a class Model:
class EventModel extends Model{
final int desavanjeId;
final String desavanjeName;
EventModel({required this.desavanjeId, required this.desavanjeName});
#override
factory EventModel.fromJson(Map<String, dynamic> data) => EventModel(
desavanjeId: data['desavanjeId'],
desavanjeName: data['desavanjeName'],
);
#override
Map<String, Object> toJson() => {
'desavanjeId': this.desavanjeId,
'desavanjeName': this.desavanjeName,
};
}
And in service I have something like this:
Future<APIResponseModel> get<T>(Uri uri) async{
APIResponseModel apiRespone = APIResponseModel();
try {
Response response = await _client.get(uri);
Map<String, dynamic> data = jsonDecode(response.body);
apiRespone.addData(T.fromJson(data));
} catch (e) {
print(e);
}
return apiRespone;
}
And I am willing to use method get in this way:
get<EventModel>(Uri('...'));
But the problem is that IDE doesn't allow me to use static method fromJson in this way I need.
And I don't want to solve this problem in this way:
switch(Model){
case EventModel:
EventModel.fromJson(data)
}
Is there any other solution for this, but to keep a syntax in this way?
You cannot call a factory constructor or a static method from a generic type in Dart. Your only solution to obtain a similar result would be to use a callback method which will create your object. Here is a possible implementation you could use:
Code Sample
/// By looking at your implementation the Model class should be
/// abstract as it is your base model and should not be able to
/// be instantiated.
abstract class Model {
// fromJson is removed as it will be a static method
Map<String, dynamic> toJson();
}
class EventModel extends Model {
final int desavanjeId;
final String desavanjeName;
EventModel({required this.desavanjeId, required this.desavanjeName});
/// fromJson is now a static method which will return an instance of
/// your constructor so you can still call it like
/// this: EventModel.fromJson()
static EventModel fromJson(Map<String, dynamic> data) => EventModel(
desavanjeId: data['desavanjeId'],
desavanjeName: data['desavanjeName'],
);
#override
Map<String, Object> toJson() => {
'desavanjeId': this.desavanjeId,
'desavanjeName': this.desavanjeName,
};
}
/// Now your method takes a dynamic type which extends your base class Model
/// And you are passing a createCallback parameter which is a Function taking
/// a Map<String, dynamic> as its single parameter and returns an object
/// of type T it will be your method fromJson.
Future<APIResponseModel> get<T extends Model>(
Uri uri, T Function(Map<String, dynamic>) createCallback) async {
APIResponseModel apiRespone = APIResponseModel();
try {
Response response = await _client.get(uri);
final data = jsonDecode(response.body) as Map<String, dynamic>;
apiRespone.addData(createCallback(data));
} catch (e) {
print(e);
}
return apiRespone;
}
Now you should be able to make a call like this:
get<EventModel>(Uri('...'), EventModel.fromJson);
Try the full code on DartPad
I have this class
#freezed
abstract class CartEntity with _$CartEntity {
const factory CartEntity.empty(String status, String message) = _Empty;
const factory CartEntity.notEmpty(int x) = _NotEmpty;
factory CartEntity.fromJson(Map<String, dynamic> json) =>
_$CartEntityFromJson(json);
}
And this converter
class CartEntityConverter
implements JsonConverter<CartEntity, Map<String, dynamic>> {
const CartEntityConverter();
#override
CartEntity fromJson(Map<String, dynamic> json) {
//the problem here
print(json);// null
return _Empty.fromJson(json);
}
#override
Map<String, dynamic> toJson(CartEntity object) {
return object.toJson();
}
}
And this wrapper class
#freezed
abstract class CartEntityWrapper with _$CartEntityWrapper {
const factory CartEntityWrapper(#CartEntityConverter() CartEntity cartEntity) =
CartEntityWrapperData;
factory CartEntityWrapper.fromJson(Map<String, dynamic> json) =>
_$CartEntityWrapperFromJson(json);
}
And iam called
final cartEntity = CartEntityWrapperData.fromJson({'x':'y'});
print(cartEntity);
fromJson method which in CartEntityConverter is always receive null json so what's i made wrong ?
Instead of making yet another converter class that you use directly, you could just add .fromJsonA method in the main class.
It will looks like this one:
#freezed
abstract class CartEntity with _$CartEntity {
const factory CartEntity.empty(String status, String message) = _Empty;
const factory CartEntity.notEmpty(int x) = _NotEmpty;
factory CartEntity.fromJson(Map<String, dynamic> json) =>
_$CartEntityFromJson(json);
factory CartEntity.fromJsonA(Map<String, dynamic> json) {
if (/*condition for .empty constructor*/) {
return _Empty.fromJson(json);
} else if (/*condition for .notEmpty constructor*/) {
return _NotEmpty.fromJson(json);
} else {
throw Exception('Could not determine the constructor for mapping from JSON');
}
}
}
solved by using
final cartEntity = CartEntityConverter().fromJson({'x':'y'});
print(cartEntity);
instead of
final cartEntity = CartEntityWrapperData.fromJson({'x':'y'});
print(cartEntity);
documentation have a lack at this point i tried random stuffs to make it work