Union of Modddels: how to properly handle narrowed types? #8
-
Sorry for the continuous streak of questions, I got hyped! Using my repo from #6 as context, I now face a rather annoying issue regarding
Here it is, explained by comments: // [… imports … ]
class Map extends ConsumerWidget {
const Map({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final logger = ref.read(loggerProvider);
final provider = mapLayerControllerProvider(layerName: "some_raster_layer");
final rasterLayer = ref.watch(provider);
return rasterLayer.when(data: (layer) {
logger.d(layer);
// ==> layer exposed as a ValidRaster, which is expected, for it was created
// with MapLayer.raster(…) with valid attributes values;
// but this fails because Dart thinks it’s a MapLayer, which has no position attribute:
// logger.i(layer.position.value);
// ==> Hence doing a cumbersome type casting:
final l = layer as ValidRaster;
logger.i(l.position.value); // works fine now!
return Text("OK");
}, error: (e, _) {
logger.d(e);
return Text("KO");
}, loading: () {
logger.d("Loading…");
return Text("Loading…");
});
}
} Of course, the problem stays the same if I, say, return rasterLayer.when(data: (layer) {
return layer.mapValidity(valid: (layer) {
logger.d(layer.runtimeType); // ValidRaster
// logger.i(layer.position.value); // Error: The getter 'position' isn’t defined for the type 'ValidMapLayer'.
logger.i((layer as ValidRaster).position.value); // works fine, for ValidRaster extends MapLayer
// with Raster, which has the attributes
return Text("Valid layer $layer");
}, invalid: (layer) {
logger.d(layer.runtimeType); // InvalidRaster
logger.d(layer.failures); // works fine; layer is seen as an InvalidMapLayer, though
return Text("Invalid layer $layer");
});
}, error: (e, _) {
logger.d(e);
return Text("KO");
}, loading: () {
logger.d("Loading…");
return Text("Loading…");
}); |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments
-
No worries at all ! I appreciate your active participation and questions. You (almost) never need to use type casting when using modddels. The package has been made to provide compile-time safety, and type casting breaks that. Let me put your modddel here for reference. It's a union @Modddel(validationSteps: [
ValidationStep([contentValidation])
])
class MapLayer extends SimpleEntity<InvalidMapLayer, ValidMapLayer>
with _$MapLayer {
MapLayer._();
factory MapLayer.raster({
required MapLayerPosition position,
//...
}) {
return _$MapLayer._createRaster(
position: position,
//...
);
}
factory MapLayer.vector({
required MapLayerPosition position,
// ...
}) {
return _$MapLayer._createVector(
position: position,
);
}
factory MapLayer.geojson({
required MapLayerPosition position,
//...
}) {
return _$MapLayer._createGeojson(
position: position,
// ...
);
}
} There are two parts to your question. First, creating a modddel with valid arguments doesn't mean that it will have a valid type compile-time. This is simply impossible as you can't predict or know the value of an object compile-time (unless it's a constant). What it does though is allowing you to deal with all validation states in a compile-safe way. So for example, if you have a void myFunction(Raster raster) {
// Pattern matching between the different validation states
raster.map(
valid: (valid) {
// `valid` is of type `ValidRaster`
},
invalidMid: (invalidMid) {
// `invalidMid` is of type `InvalidRasterMid` which extends `InvalidRaster`
},
);
// Or if you only need to handle the "valid" case :
raster.mapOrNull(
valid: (valid) {}, // `valid` is of type `ValidRaster`
);
} We need pattern matching because we don't know if the void myFunction(ValidRaster validRaster) { //.... So you'll need to do pattern matching somewhere before calling Secondly, just like for freezed, when you create an instance of a case-modddel (for example : void myOtherFunction(MapLayer mapLayer) {
mapLayer.mapMapLayer(
raster: (raster) {}, // Of type `Raster`
vector: (vector) {}, // Of type `Vector`
geojson: (geojson) {}, // Of type `Geojson`
);
// Or if you only need to handle the `Raster` case :
mapLayer.mapOrNullMapLayer(
raster: (raster) {},
);
} Same principle applies here, you can require that In your example :
The reason you get an error is that in the beginning To get back to your specific usecase, it depends on what you need. Does your provider only hold Depending on your answer, you can make the type of the provider exactly what you need it to be, and also handle in the widget only the needed cases (consider using |
Beta Was this translation helpful? Give feedback.
-
On this note, freezed supports directly creating a union-case and preserving its type, for example : @freezed
class MapLayer with _$MapLayer {
const factory MapLayer.raster(int someParam) = Raster;
const factory MapLayer.vector(int someParam) = Vector;
const factory MapLayer.geojson(int someParam) = Geojson;
}
// then, instead of calling `MapLayer.raster(1)` :
final Raster raster = Raster(1); In modddels, this is not supported (for now). If needed, you can cast the case-modddel directly after creating it, like this : final raster = MapLayer.raster(
//...
) as Raster; Although it's pretty safe, it's not very clean so I'll look into implementing a better alternative. |
Beta Was this translation helpful? Give feedback.
-
Thank you sir! I'll try that asap. |
Beta Was this translation helpful? Give feedback.
-
Alright, you made it crystal clear what to do, and sure it unfolds naturally: class Map extends ConsumerWidget {
const Map({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final logger = ref.read(loggerProvider);
final provider = mapLayerControllerProvider(layerName: "some_raster_layer");
final someLayer = ref.watch(provider);
// TODO: load other map layers as well (as per current / user config), iterate on collection, etc.
return someLayer.when(data: (mapLayer) {
// here, I first mapped over cased-modddels types, then validity, but one could do it the reverse way
return mapLayer.mapMapLayer(
raster: (rasterLayer) => rasterLayer.mapValidity(
valid: (validRasterLayer) => Text(
"Valid raster layer! ${validRasterLayer.position.value}"),
invalid: (invalidRasterLayer) =>
Text("Invalid raster layer! ${invalidRasterLayer.failures}")),
vector: (vectorLayer) => Text("Vector layer! TODO: mapValidity"),
geojson: (geojsonLayer) => Text("GeoJSON layer! TODO: mapValidity"));
}, error: (e, _) {
logger.d(e);
return Text("KO");
}, loading: () {
logger.d("Loading…");
return Text("Loading…");
});
}
} What I was mostly missing is the existence of Now it’s just a matter of organizing widget’s controllers, and eventually services/repositories, so as to keep the widgets light and only concerned with displaying either the expected content or a user-friendly error. Thank you again! |
Beta Was this translation helpful? Give feedback.
-
Awesome, glad to hear you're getting the hang of it !
That's exactly the kind of smooth usage I was aiming for with modddels. I hope you enjoy using the package, and feel free to reach out if you run into any more questions or issues. |
Beta Was this translation helpful? Give feedback.
No worries at all ! I appreciate your active participation and questions.
You (almost) never need to use type casting when using modddels. The package has been made to provide compile-time safety, and type casting breaks that.
Let me put your modddel here for reference. It's a union
MapLayer
with three case-modddels :Raster
,Vector
andGeojson
.