Is it possible to declare a Map type that requires all enum values to be present as keys? - flutter

How can I require the Dart compiler to warn me when I forget to include all members of an enum in a map? For example, in the following:
enum Size {
small,
medium,
large,
}
// This is a valid Dart code. Dart compiler doesn't require `Size.large` to be present.
final Map<Size, ButtonSize> sizeMap = {
Size.small: const MyClass(),
Size.medium: const MyClass(),
};
The Dart compiler isn't that restrictive. It doesn't require all enum values to be present in the Map, so I can't be sure that the following code will return an instance of MyClass. It might resolve to null:
final MyClass instance = sizeMap[Size.small]; // unsafe
I have to either do this:
final MyClass? instance = sizeMap[Size.small]; // `instance` might be `null`
or this:
final MyClass instance = sizeMap[Size.small] as MyClass; // `instance` might still be `null`, but we're pretending it's not.
Both solutions are far from perfect. The first one implies further null checks in the code, the second one smells because of typecasting.
Is there any way to declare the type of sizeMap so that all enum values must be present?

Instead of using a Map, you'd be better off with a function and a switch statement:
enum Size {
small,
medium,
large,
}
class MyClass {
const MyClass();
}
MyClass mapSize(Size size) {
switch (size) {
case Size.small:
return const MyClass();
case Size.medium:
return const MyClass();
}
}
the above code generates a compile-time error from not handling all enum values:
Error: A non-null value must be returned since the return type 'MyClass' doesn't allow null.
With null-safety, the above code cannot be executed until the above problem is fixed.
Additionally, dart analyze will produce a more descriptive warning:
Missing case clause for 'large'. Try adding a case clause for the missing constant, or adding a default clause. • missing_enum_constant_in_switch
If you want to use a Map because you want to allow it to be mutated, you could provide separate functions to set and get values:
var _smallValue = const MyClass();
var _mediumValue = const MyClass();
var _largeValue = const MyClass();
MyClass mapSize(Size size) {
switch (size) {
case Size.small:
return _smallValue;
case Size.medium:
return _mediumValue;
case Size.large:
return _largeValue;
}
}
void setMappedSize(Size size, MyClass value) {
switch (size) {
case Size.small:
_smallValue = value;
break;
case Size.medium:
_mediumValue = value;
break;
case Size.large:
_largeValue = value;
break;
}
}
If you find that to be too verbose, and you just want callers to avoid returning null, you could wrap a Map internally:
const _defaultValue = const MyClass();
final _sizeMap = <Size, MyClass>{};
MyClass mapSize(Size size) => _sizeMap[size] ?? _defaultValue;
void setMappedSize(Size size, MyClass value) => _sizeMap[size] = value;

Related

Why enums can't be used in const constructors in Flutter?

can someone tell me if there's a way to use enhanced enums in const constructors? I tried both named constructors and factory constructors, with no luck:
enum MyEnum {
first('first-string'),
second('second-string');
final String string;
const MyEnum(this.string);
}
class MyClass {
final String input;
const MyClass({required this.input});
const MyClass.first({
this.input = MyEnum.first.string, //error
});
factory MyClass.second() {
return const MyClass(input: MyEnum.second.string); //error
}
}
Named constructor gives error: The default value of an optional parameter must be constant
Factory constructor gives error: A value of type 'Null' can't be assigned to a parameter of type 'String' in a const constructor. Try using a subtype, or removing the keyword 'const'
Right now the only solution I found was to replace enum with a class containing static const params, like this:
class MyEnumClass {
static const String first = 'first-string';
static const String second = 'second-string';
}
You can use enums if you use the enum type itself for the member.
enum MyEnum {
first('first-string'),
second('second-string');
final String string;
const MyEnum(this.string);
}
class MyClass {
final MyEnum _input;
String get input => _input.string;
const MyClass({required MyEnum input}) : _input = input;
const MyClass.first({
MyEnum input = MyEnum.first,
}) : _input = input;
factory MyClass.second() {
return const MyClass(input: MyEnum.second);
}
}
In general, getters are equivalent to function calls and aren't constant expressions. (There are a some exceptions, and maybe enum members should be too.)

How to create generic class of type T, where T is Type variable?

I'm trying to create list of generic typed class from mixed-type list.
It works, but the problem is each Data instance type is dynamic.
class Data<T> {
final T value;
const Data(this.value);
}
final List<dynamic> bases = [...];
final List<Data> data = bases.map((b) {
return Data(b);
}).toList();
Attempt #2
class Data<T> {
final T value;
const Data(this.value);
}
final List<dynamic> bases = [...];
final List<Data> data = bases.map((b) {
final Type T = b.runtimeType;
return Data<T>(b);
}).toList(); // List<Data<dynamic>>
But it fails, due to: The name 'T' isn't a type so it can't be used as a type argument. Try correcting the name to an existing type, or defining a type named
Generic type parameters must be known statically (i.e., at compile-time). If you start off with a heterogeneous List<dynamic>, then the static type of each element is dynamic, and therefore when you construct a Data object from that element, it will be Data<dynamic>.
If your heterogeneous list has a limited number of types, you could do:
var data = <Data<dynamic>>[];
for (var b in bases) {
if (b is Foo) {
var d = Data(b); // `d` is of type `Data<Foo>`
data.add(d);
} else if (b is Bar) {
var d = Data(b); // `d` is of type `Data<Bar>`
data.add(d);
} else {
throw UnimplementedError('Unrecognized type: ${b.runtimeType}');
}
}
(Note that if you do data.add(Data(b)) in the above, the Data object will be constructed as Data<dynamic> because the generic type parameter will be inferred from data (of type List<Data<dynamic>>) instead of from b.)
Otherwise what you want isn't really possible.

Why does the dart "is" operator behave differently for local variables vs. class fields? [duplicate]

This question already has answers here:
"The operator can’t be unconditionally invoked because the receiver can be null" error after migrating to Dart null-safety
(3 answers)
Closed 12 months ago.
I have migrated my Dart code to NNBD / Null Safety. Some of it looks like this:
class Foo {
String? _a;
void foo() {
if (_a != null) {
_a += 'a';
}
}
}
class Bar {
Bar() {
_a = 'a';
}
String _a;
}
This causes two analysis errors. For _a += 'a';:
An expression whose value can be 'null' must be null-checked before it can be dereferenced.
Try checking that the value isn't 'null' before dereferencing it.
For Bar() {:
Non-nullable instance field '_a' must be initialized.
Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.
In both cases I have already done exactly what the error suggests! What's up with that?
I'm using Dart 2.12.0-133.2.beta (Tue Dec 15).
Edit: I found this page which says:
The analyzer can’t model the flow of your whole application, so it can’t predict the values of global variables or class fields.
But that doesn't make sense to me - there's only one possible flow control path from if (_a != null) to _a += 'a'; in this case - there's no async code and Dart is single-threaded - so it doesn't matter that _a isn't local.
And the error message for Bar() explicitly states the possibility of initialising the field in the constructor.
The problem is that class fields can be overridden even if it is marked as final. The following example illustrates the problem:
class A {
final String? text = 'hello';
String? getText() {
if (text != null) {
return text;
} else {
return 'WAS NULL!';
}
}
}
class B extends A {
bool first = true;
#override
String? get text {
if (first) {
first = false;
return 'world';
} else {
return null;
}
}
}
void main() {
print(A().getText()); // hello
print(B().getText()); // null
}
The B class overrides the text final field so it returns a value the first time it is asked but returns null after this. You cannot write your A class in such a way that you can prevent this form of overrides from being allowed.
So we cannot change the return value of getText from String? to String even if it looks like we checks the text field for null before returning it.
An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.
It seems like this really does only work for local variables. This code has no errors:
class Foo {
String? _a;
void foo() {
final a = _a;
if (a != null) {
a += 'a';
_a = a;
}
}
}
It kind of sucks though. My code is now filled with code that just copies class members to local variables and back again. :-/
Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.
Ah so it turns out a "field initializer" is actually like this:
class Bar {
Bar() : _a = 'a';
String _a;
}
There are few ways to deal with this situation. I've given a detailed answer here so I'm only writing the solutions from it:
Use local variable (Recommended)
void foo() {
var a = this.a; // <-- Local variable
if (a != null) {
a += 'a';
this.a = a;
}
}
Use ??
void foo() {
var a = (this.a ?? '') + 'a';
this.a = a;
}
Use Bang operator (!)
You should only use this solution when you're 100% sure that the variable (a) is not null at the time you're using it.
void foo() {
a = a! + 'a'; // <-- Bang operator
}
To answer your second question:
Non-nullable fields should always be initialized. There are generally three ways of initializing them:
In the declaration:
class Bar {
String a = 'a';
}
In the initializing formal
class Bar {
String a;
Bar({required this.a});
}
In the initializer list:
class Bar {
String a;
Bar(String b) : a = b;
}
You can create your classes in null-safety like this
class JobDoc {
File? docCam1;
File? docCam2;
File? docBarcode;
File? docSignature;
JobDoc({this.docCam1, this.docCam2, this.docBarcode, this.docSignature});
JobDoc.fromJson(Map<String, dynamic> json) {
docCam1 = json['docCam1'] ?? null;
docCam2 = json['docCam2'] ?? null;
docBarcode = json['docBarcode'] ?? null;
docSignature = json['docSignature'] ?? null;
}
}

Dart : Read annotated value of a field

I have a Dart enum which looks like this:
enum Gender {
#JsonValue(0)
male,
#JsonValue(1)
female,
}
I have created a dart extension that returns the String name and int value. It looks something like this -
extension GenderExtention on Gender {
String get name {
switch (this) {
default:
return _getDefaultName(this);
}
}
//For the enum male, it returns "Male"
String _getDefaultName(Community value) {
if (value == null) {
return null;
}
String valueStr = value.toString();
String enumName = valueStr.substring(valueStr.indexOf('.') + 1);
return enumName[0].toUpperCase() + enumName.substring(1);
}
int get value {
switch (this) {
case Gender.male:
return 0;
case Gender.female:
return 1;
default:
return null;
}
}
}
This becomes painful for larger enums, especially the value section.
Are there any suggestions on how to get the enum value (0 for #JsonValue(0)) more easily than manually defining it in the extension? Would using something like reflections help here?
the only way to access annotations at run-time is indeed reflection using dart:mirrors. That library is not available when compiling to the web or for Flutter, so it's probably not going to solve your problem.
What you can do here is:
int get value => this.index;
That only works if the values are actually the same as the index (0 for the first declared value, 1 for the next, etc.)

Flutter: create an Object from Type or String

I want to create an Object from an object type defined in Type or from an object name defined in String. The following example uses a String to hold the object type. But I think this is not that elegant - even with Type, this if block would increase dramatically for a lot of object types...
I didn't find a better solution for this yet. How can I create that object dynamically from the specified object type?
if (model == 'Event') {
data = Event.fromMap(result);
}
else if (model == 'Content') {
data = Content.fromMap(result);
}
else if (...) {
// ...
}
This is other approach.
class Event{
Event.fromMap(_map){
print('This is an event');
print(_map);
}
}
class Content{
Content.fromMap(_map){
print('This is a content');
print(_map);
}
}
Map<String, Function> types = {
'Event' : (_map)=>Event.fromMap(_map),
'Content' : (_map)=>Content.fromMap(_map),
};
void main() {
var a = types['Event']({'test':'success_event'});
print(a.runtimeType);
var b = types['Content']({'test':'success_content'});
print(b.runtimeType);
}
Its a bit more scalable (since only depends on add the class constructor into the map).
The explanation:
class Event{
Event.fromMap(_map){
print('This is an event');
print(_map);
}
}
class Content{
Content.fromMap(_map){
print('This is a content');
print(_map);
}
}
Here we are creating the test classes. nothing important.
Map<String, Function> types = {
'Event' : (_map)=>Event.fromMap(_map),
'Content' : (_map)=>Content.fromMap(_map),
};
Here we are defining a Map. Why? Because it allows us to access some value through some key in constant time. In this case, the keys are the Strings 'Event', 'Content', but also can be types as you wanted. For simplicity, let them be Strings. The values are Function's, in this example only getting as parameter a _map (because the Class constructors in the example require one parameter _map). So, if you need more types, only add the type and the function encapsulating the constructor for that type.
void main() {
var a = types['Event']({'test':'success_event'});
print(a.runtimeType);
var b = types['Content']({'test':'success_content'});
print(b.runtimeType);
}
Finally you can instantiate the classes easily. Only with the type string and passing to the function the values you want (In this example a map with a key 'test').
In your example would be something like:
data = types[model](result);