I'm trying to figure out if my updated code is the correct way to use a factory constructor with null safety. I reviewed stackoverflow and the Dart.dev language tour to try to better understand factory constructors. I have struggled to apply the concepts outlined to my code. I'm new to Flutter, Dart and coding. This is my first attempt at using a factory constructor so the primary issue is my lack of understanding and not any issues with the answers on stackoverflow or elsewhere.
After reading a lot I settled on the approach in the code below marked as Updated. The errors are now all gone and my app is behaving as I want but my fear is throwing an error instead of returning null may not be a sound approach. My approach just looks wrong to my beginner eyes. My goal is for my code to work and to also understand why I am using whatever approach I am using so that I can apply that knowledge to future situations. I can provide any additional code that may be needed to comment. Thanks in advance for the help.
Original Code that throws an error
class Job {
Job({required this.name, required this.ratePerHour});
factory Job.fromMap(Map<String, dynamic>? data) {
if (data == null) {
return null;
}
final String name = data['name'];
final int ratePerHour = data['ratePerHour'];
return Job(name: name, ratePerHour: ratePerHour);
}
final String name;
final int ratePerHour;
Map<String, dynamic> toMap() {
return {
'name': name,
'ratePerHour': ratePerHour,
};
}
}
Updated code that works
class Job {
Job({required this.name, required this.ratePerHour});
factory Job.fromMap(Map<String, dynamic>? data) {
if (data != null) {
final String name = data['name'];
final int ratePerHour = data['ratePerHour'];
return Job(name: name, ratePerHour: ratePerHour);
} else {
throw ArgumentError('Data is null');
}
}
final String name;
final int ratePerHour;
Map<String, dynamic> toMap() {
return {
'name': name,
'ratePerHour': ratePerHour,
};
}
}
Your own solution works fine however, I would check data before calling the factory.
factory Job.fromMap(Map<String, dynamic> data) {
return Job(name: data['name'], ratePerHour: data['ratePerHour']);
}
But then again why not use data and call the regular constructor?
Job? job;
if (data != null) {
job = Job(name: data['name'], ratePerHour: data['ratePerHour'])
}
https://dart.dev/guides/language/language-tour#factory-constructors
Factory constructors
Use the factory keyword when implementing a
constructor that doesn’t always create a new instance of its class.
For example, a factory constructor might return an instance from a
cache, or it might return an instance of a subtype. Another use case
for factory constructors is initializing a final variable using logic
that can’t be handled in the initializer list.
You've pointed out several true and good facts, and I feel like that you're on the right way to implement this.
I also feel like there's no straight "right" answer to this question; I think this also connects to concepts as clean code and clean architecture, which are broader than Dart and Flutter themselves
You can either:
Throw and let the caller (upper layer) handle that problem;
Print some logs and return a zero-value to the caller (in your case, an "empty" object).
Case 1 is desirable if you don't want to handle cases like that one.
Case 2 is desirable if you can afford to return something weird like a Job("job name",0) and still be good.
It really depends on what you're building. By looking at your context, I'd probably go with option 1 and try/catch that in a middle layer (maybe you want to show your user "An error occured" whenever data is null)?
Nonetheless, you might need to refactor this feature in a way that allows you not to encounter these edge cases. There's a good chance dependency inversion is your friend, here.
Related
So this is a Flutter application using Dart language with the Riverpod package for state management. The intent is that an application has a user if, they are signed in, and is set to null if they are not (or signed out). I found that setting the user to null did not notify the listeners, so I tried with a basic nullable String field, called name. I received the same result.
Example below is for the simple nullable String field. Another thing is, I use the standard data class generator plugin to generate all the boiler plate code such as equality, copywith, hashcode and so on.
So let's assume I have the following using flutter_riverpod: 2.1.3
class AppSettings {
User? user;
String? name;
AppSettings({
this.user,
this.name,
});
AppSettings copyWith({
User? user,
String? name,
}) {
return AppSettings(
user: user ?? this.user,
name: name ?? this.name,
);
}
// Additional data class generator methods
}
class AppSettingsNotifier extends StateNotifier<AppSettings> {
AppSettingsNotifier() : super(AppSettings());
void updateUser(User? user) {
state = state.copyWith(user: user);
}
void updateName(String? name) {
state = state.copyWith(name: name);
}
}
final appSettingProvider =
StateNotifierProvider<AppSettingsNotifier, AppSettings>(
(ref) => AppSettingsNotifier());
Then when I set the name field as follows:
ref.read(appSettingProvider.notifier).updateName(null);
Then my listeners aren't reacting and the widgets aren't rebuilt.
On the other hand, if I set the name to an actual string:
ref.read(appSettingProvider.notifier).updateName("Bruce Lee");
It would instantly update. Even with empty string it will also notify the listeners. So it seems something special is happening with null specifically.
Why is "null" not causing a notification to listeners?
What am I missing here?
I've tried reading the manual, googling and countless attempts at debugging. Unfortunately, I do not understand enough of the underlying Riverpod/Flutter code to get to the bottom of it.
By looking at the provided code, So far what I can see a potential issue in your copyWith method. Bcz in flutter, when using a copyWith method the common convention is to keep the instances past data if the data given inside the copyWith is null.
Right now, it looks like that can be the issue here. So when you pass a null data, The new instance comes with the name value from your past object. So try to check if it's the case in your AppSettings model class.
I am trying to connect a Hive database with domain and a data layer. However, I am struggling to transform my entity data class to a HiveObject. With my current approach I receive the error as seen in the title of this post. Other attempts also failed and I start recognizing, I appear to have a lack of understanding, how those constructors work...
So in general, I want to use a class ShProblem in the domain layer that has a constructor
static ShProblem fromEntity(ShmProblemEntity entity) {
return ShProblem(
id: entity.id,
level: entity.level,
);
}
This constructor shall receive/provide data from/to the data source via an entity class. Getting data from Hive should work (not tested, but at least no compile error), however, the other way around does not work as with JSON maps (seen in the toDocument constructor), what I used previously with other databases.
class ShmProblemEntity {
ShmProblemEntity({required this.id, required this.level, });
final int id;
final int level;
static ShmProblemEntity fromHive(ShProblemHive hiveObject) {
return ShmProblemEntity(
id: hiveObject.id,
level: hiveObject.level,
);
}
static ShProblemHive toHive() => {
ShProblemHive(
id: this.id,
level: level,
)
};
Map<String, dynamic> toDocument() => {
"id": id,
"level": level,
};
So, it is strange to me that fromHive throws no error while toHive does, though I notice the difference that fromHive has the same datatype as the class of the constructor while toHive has not.
I am looking for a good way to validate freezed models. So far I came up with three approaches, which are shown in the snippet below.
#freezed
class Options with _$Options {
Options._();
factory Options._internal({required List<String> languages}) = _Options;
// #1: validation in factory constructor
factory Options({required List<String> languages}) {
if (languages.isEmpty) {
throw Exception('There must be at least one language.');
}
return Options._internal(languages: languages);
}
// #2: expose mutation methods with built-in validation
Options changeLanguages(List<String> languages) {
if (languages.isEmpty) {
throw Exception('There must be at least one language.');
}
return copyWith(languages: languages);
}
// #3: validation using custom properties
late final List<Exception> validationResult = <Exception>[
if (languages.isEmpty) Exception('There must be at least one language.'),
];
// #4: validation using a custom method
void validate() {
if (languages.isEmpty) {
throw Exception('There must be at least one language.');
}
}
}
#1: Validation inside a factory constructor. Unfortunately, this only works for newly created objects and requires further changes for copyWith.
#2: Validation inside a mutation method. This could be used in addition to #1 to run validation after object creation, but still does not work for copyWith.
#3: Exposing a property with validation errors. So far, this is my favorite approach, even though it requires users of the model to explicitly look for errors.
#4: A variation of #3, which uses a throwing method instead of providing a list of errors.
What are your thoughts on this? Do you know any better approaches or is there a part of the package API, which I have overlooked?
Freezed added support for custom asserts in v0.12.0: https://pub.dev/packages/freezed#asserts. Applying these to your example results in the following:
#freezed
abstract class Options with _$Options {
Options._();
#Assert('languages.isNotEmpty', 'There must be at least one language.')
factory Options({required List<String> languages}) = _Options;
}
However, this doesn't allow you to throw arbitrary exceptions, and asserts are only included in debug builds, not in profile/release builds.
I would probably move the validation one step below. You can create a model LanguageList:
class LanguageList {
LanguageList(this.data) {
if (data.isEmpty) throw ArgumentError('Provide at least one element');
}
final List<String> data;
#override
bool operator ==(Object other) => other is LanguageList && ListEquality<String>.equals(data, other.data);
#override
int get hashCode => DeepCollectionEquality().hash(data);
}
and use it in the Options model:
factory Options._internal({required LanguageList languages}) = _Options;
You can even make it more "compile-friendly" by making illegal states unrepresentable instead of throwing an error in runtime:
class LanguageList {
LanguageList(String head, Iterable<String> tail) : data = [head, ...tail];
final List<String> data;
#override
bool operator ==(Object other) => other is LanguageList && ListEquality<String>.equals(data, other.data);
#override
int get hashCode => DeepCollectionEquality().hash(data);
}
In this case there's just no way to create wrong instance.
To make invalid state unrepresentable, you need constructor validation. If the class is mutable, you need to validate when mutated as well, of course. None of the solutions above seem all that helpful. The best solution I have is to not use Freezed. Freezed is a great package that does really a lot. But, it maybe YAGNI in many cases. So, if you don't need all the features, bring in packages that provide just those you actually do need and hand roll those that for which there is no package.
See this discussion with Remi: https://github.com/rrousselGit/freezed/issues/830
Been doing some research on Flutter Dependency Injection,and I kinda settled on Inject.dart
However, I am having some trouble using inject.
Is there any way to simplify instantitation of a injected class?
I have this MyClass, which I need to instantiate passing a HomeStore, however, how can I instatiate it without calling global acessors? (such as the ones made on the Injector file).
It seems I could just use the Get_It package otherwise, which would get me the same results, and without code generation, but I don't quite like the whole global access thing.
My sample:
// current code
class MyClass(){
store = HomeStore(AppInjector().globalHudStore, AppInjector().globalErrorStore);
}
// desired code
class MyClass(){
#instance ?
store = HomeStore();
store = HomeStore.instanciate?();
}
class HomeStore {
#provide
HomeStore(this._globalHudStore, this._globalErrorStore);
final GlobalHudStore _globalHudStore;
final ErrorStore _globalErrorStore;
}
If you are willing to work with json, you might want to consider this package json_serializable. Just search for it at pub.dev.
I can give a little example. Let's say you have a User class/model.
part 'user.g.dart';
#JsonSerializable()
class User {
int id;
#JsonKey(name: 'user_type_id')
int userTypeId;
String email;
#JsonKey(name: 'user_detail')
UserDetail userDetail;
#JsonKey(name: 'user_type')
UserType userType;
#JsonKey(name: 'user_location')
UserLocation userLocation;
User();
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
and you fetched a User data via API, and want to create a User variable and fill it with data without having to do that manually.
You can actually do that with just this line of code
User user = User.fromJson(jsonUser);
These even automatically json serializes classes in your User class, as long as those classes are also json serializable, which is defined with #JsonSerializable()
So even with just that single line of code, if the jsonUser which came from the API also has values for UserDetail, UserType, and UserLocation, you can also access them.
Looking for help for assigning List values to a class. Here is my class.
class Specialties {
int id;
String name;
String details;
const Specialties(this.id, this.name, this.details);
}
I have a List created from JSON and I have confirmed it has values, but can not get the values assigned to instantiate the class. I know the class works as when I hardcode values it gets created just fine.
This is how I am trying to do it and can not find why it does not work.
Specialties getSpec(int index) {
return new Specialties(mylist[index].id, mylist[index].name, mylist[index].details);
}
I am sure I am missing something easy, but once I saw hardcoded values working, can not figure it out.
Any help would be great.
It seems like you may be coming from a JavaScript background where object.property
object['property'] are equivalent. In Dart, they are not.
If you parsed a JSON object it will turn into a Dart Map with String keys. Modify your getSpec function to use operator[] instead of dot syntax to read the entries of the Map.
Specialties getSpec(int index) {
return new Specialties(mylist[index]['id'], mylist[index]['name'], mylist[index]['details']);
}
You may want to consider giving Specialties an additional fromMap constructor rather than constructing it in an external function. You should also make the fields of Specialties be final since its constructor is const.
class Specialties {
final int id;
final String name;
final String details;
const Specialties(this.id, this.name, this.details);
factory Specialties.fromMap(Map data) => new Specialties(data['id'], data['name'], data['details']);
}
Then you can say
new Specialties.fromMap(mylist[index])
Finally, there are some other JSON deserialization libraries that you might want to consider to reduce the amount of boilerplate deserialization code: Jaguar, built_value.
If you have a JSON string in the form of '[{"id": 1, "name": "ab", "details": "blabla"},{...}]' you can convert it like this:
List<Specialties> specialties(String jsonstring) {
List parsedList = JSON.decode(jsonstring);
return parsedList.map((i) => new Specialties(
i["id"], i["name"], i["details"]));
}
You will have to import dart:convert to get the method JSON.decode