How can I iterate a collection (e.g. Map<String, supertype>) and render a separate widget for each subtype in the collection? - flutter

I have a list of objects that are all subtypes of Animal. I cast them all to supertype in the animal list to keep their references in one place. So far so good.
Now, when I render state information about each animal object I would like to use different widgets based on the subtype of each Animal object. When I cast from supertype to subtype, even when I specify the field is dynamic, I get the error.
Type 'type' is not a subtype of type '<subtype>'
It seems that the dart compiler uses the type information from the animalTiles map to enforce that all elements are of type Animal, even if the constructor that I pass the type into takes a dynamic named parameter, which I hoped would be looser.
// ANIMAL LIST
class AnimalList extends StatelessWidget {
final Map<String, Animal> animals;
#override
Widget build(BuildContext context) {
List<Widget> animalTiles = [];
animals.forEach(
(key, setting) => animalTiles.add(
AnimalTile(animal: animal),
),
);
return Column(
children: animalTiles,
);
}
}
// ANIMAL TILE
class AnimalTile extends StatelessWidget {
AnimalTile({
required animal,
}) {
_buildDescription(animal);
}
late dynamic animal;
late Widget description;
Widget _buildDescription(dynamic setting) {
if (animal.type == Animal.CAT) {
Cat cat = animal;
return CatWidget(cat: cat);
} else if (animal.type == Animal.DOG) {
Dog dog = animal;
return DogWidget(dog: dog);
} else if (animal.type == Animal.MOUSE) {
Mouse mouse = animal;
return MouseWidget(mouse: mouse);
} else {
return Container(child: Text(':('));
}
}
#override
Widget build(BuildContext context) {
return(
...
);
}
}
The Dart type system requires that I initialize final fields before the constructor body in an initialization list or assign directly to instance fields in the params list. If I call a helper method in the body of my constructor, that is not considered final so I would not like to use this for an immutable Stateless Widget.
This has to be a somewhat common pattern..I am trying to keep all subtypes in the same list and then use the subtype type to display different widgets. Why does this approach fail and what is a valid way to implement this pattern in Dart?

The Flutter Cookbook has an example of creating a list of items of different subtypes. However I find myself repeating data fields that I was hoping would be common to the base class. Because, if I use extends in place of implements (i.e. inheritance over interface) for the abstract Animal class, I get an Exception indicating that Dart is looking at the method of the base class, not the overridden method in the subclass. This is not the best solution for keeping your code DRY, but it works.
Unfortunately, this means that I do not have a solution that enables me to keep data fields used by my many animals within a single class, but I am posting this to demonstrate a pattern that works.
UPDATE:
You may also be able to reuse data-fields across subtypes while ensuring that the methods called are those overridden by the subtype using both extends (inheritance) and implements (interface).
Here is my solution, please outdo me:
// Base Class
abstract class AnimalBase {
AnimalBase({
required this.height,
required this.weight,
required this.name,
required this.genus,
required this.species,
});
final int height;
final double weight;
final String name;
final String genus;
final String species;
static const CAT = 0;
static const DOG = 1;
static const MOUSE = 2;
static AnimalFactory(Map<String, dynamic> json, int type) {
// IN MY CASE I PASS THE TYPE TO A FACTORY
switch (type) {
case CAT:
return Cat.fromJson(json);
case DOG:
return Dog.fromJson(json);
case MOUSE:
return Mouse.fromJson(json);
default:
return null;
}
}
}
// Interface Class
abstract class Animal; {
Widget buildTile();
Widget description();
}
// Example SubType
class Dog extends AnimalBase with Animal {
Dog({
required height,
required weight,
required name,
required genus,
required species,
}) : super(height: height, weight: weight, name: name, genus: genus, species: species);
#override
Dog.fromJson(Map<String, dynamic> json)
throw new UnimplementedError();
}
Map<String, dynamic> toJson() {
throw new UnimplementedError();
}
#override
Widget buildTile() {
return DogTile(dog: this);
}
#override
Widget description() {
return DogDescription(dog: this);
}
}
class AnimalList extends StatelessWidget {
AnimalList({
Key? key,
required this.animals,
}) : super(key: key);
final Map<String, Animal> animals;
#override
Widget build(BuildContext context) {
List<Widget> animalTiles = [];
animals.forEach(
(key, animal) => animalTiles.add(
AnimalTile(animal: animal),
),
);
return CustomScrollView(
shrinkWrap: true,
slivers: [
SliverToBoxAdapter(
child: Column(
children: ...animalTiles.map(animal => animal.buildTile()).toList(),
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Divider(
color: Colors.black54,
height: 18,
thickness: 4,
indent: 20,
endIndent: 20,
),
],
),
),
],
);
}
}
I wish I had a solution that only required one keyword (extends or implements) but this is the best I have :)

Related

Enum Generic not allow typed enum

I created a class that accepts a generic enum as one of it's parameters. However when I then go to pass in a specified enum type, it throws the error Expected a value of type '(Enum?) => void', but got one of type '(GradeLevel?) => void'.
My classes are:
Radio Group Presentable
class RadioGroupPresentable<T extends Enum> implements Presentable {
final List<T> values;
final T groupValue;
void Function(T? value) onChanged;
RadioGroupPresentable(this.values, this.groupValue, this.onChanged);
}
RadioGroup
class RadioGroup<T extends Enum> extends StatelessWidget {
final List<T> _values;
final T _groupValue;
ValueChanged<T?> onChanged;
RadioGroup(this._values, this._groupValue, this.onChanged, {super.key});
RadioGroup.fromPresentable(RadioGroupPresentable<T> p): this(p.values, p.groupValue, p.onChanged);
#override
Widget build(BuildContext context) {
print("building radio grouo");
return Column(
children: _values.map((e) {
return ListTile(
title: Text(e.name),
leading: Radio<T>(
value: e,
groupValue: _groupValue,
onChanged: onChanged,
),
);
}).toList(),
);
}
}
Enum
enum GradeLevel {
tenth, eleventh, twelfth
}
Presentable creation
RadioGroupPresentable(GradeLevel.values, GradeLevel.tenth, (value) {
if (value != null) {
gradeLevel.setValue(value, action: "Update Grade Level");
_application.gradeLevel = value;
print(value);
}
})
View (where error occurs)
case RadioGroupPresentable<GradeLevel>:
RadioGroupPresentable<GradeLevel> presentable = p as RadioGroupPresentable<GradeLevel>;
RadioGroup widget = RadioGroup.fromPresentable(presentable);
content.add(widget);
break;
I have tried to make the class generic so it would accept any enum, however it crashes when I pass it an enum.
The issue is caused by this line:
RadioGroup widget = RadioGroup.fromPresentable(presentable);
Type inference is failing here, so this is being interpreted as RadioGroup<Enum>.fromPresentable(presentable) rather than RadioGroup<GradeLevel>.fromPresentable(presentable).
When the RadioGroup.fromPresentable structure attempts to store the onChanged function, it finds a function that has the wrong type. It wants a function that is capable of taking an argument of any Enum type, but the function it finds is only capable of taking arguments of the GradeLevel type.
Remember, a function is only a subtype of another if its argument types are equally or more permissive than its supertype.
(Enum value) {} is void Function(GradeLevel);
(GradeLevel value) {} is! void Function(Enum);

How Do I Properly Use Equatable To Recognise Deeply Nested Data Using Flutter_bloc 8.0.0+

Introduction: I have been working on this problem for a while. The problem involves updating the state of deeply nested data with the Bloc 8.0.0+ paradigm. I am using a class that holds deeply nested data in it (in this minimum viable code: a ‘Collection’ that contains a name and children are meant to be a Show Series, and the Series has children that are meant to be Seasons, and Seasons have children that are meant to be Episodes).
The nested structure is something like this:
List<CollectionState>
|-- List<CollectionState>
| |-- List<CollectionState>
| | |-- List<CollectionState>
An important functionality of the code is that a child is added to the children list of the correct parent so it will display in the correct order of the hierarchy of parents to their children in the ListView; i.e. Collection has one Series (8768), and that Series has two Seasons (1817 and 7623), and when pressing on a Season, an Episode is added to its correct parent Season instead of being added to the bottom of the ListView. In this case, pressing on Season 1817 four times adds Episodes 2175, 2773, 5420 and 8826 under itself instead of adding to Season 7623.
Problem: As I understand it, a good practice while working with BLoC 8.0.0+ would be extending the CollectionState class with Equatable. The following code I provide works; however, it does not use this best practice. I want it to do so, but I am having problems which I will explain shortly. I have commented in the code of collection_state.dart with:
// 1) where Equatable should be extended
I have located in the code where the issue occurs in collection_bloc.dart when the CollectionState class is extended with Equatable (please note that the problem happens only with changing the code by extending CollectionState class with Equatable, which the code does not do). I have commented in the code with this comment:
//TODO: Here is the problem. This code does not work properly when I extend the CollectionState class to Equatable.
Surprisingly, there is little information online that I could find about using deeply nested data with BLoC 8.0.0+.
I am new to BLoC 8.0.0+ and even newer to Equatable (I’ve always used Provider up to this point) and I don’t understand why my code is not updating correctly when extending Equatable. I guess I am having an immutability issue because the update to the class with the AddInfo bloc event is not considered different to Equatable. I am at a loss to understand how to change my code to use the best practices with deeply nested data with BLoC.
Question:
How do I change my code to extend the CollectionState class with Equatable and still have it update my UI correctly?
Bearing in mind that I have a cursory understanding of Equatable, I would like to know more about the underlying root of the problem. Is it the case that the bloc event method is not producing a class different enough to Equatable, so it is not updating or is something entirely different happening here?
Note: When I extend Equatable and add the props for name, children, showType; and click on the collection, it adds Series as normal. But, when I click on Series, the app does not update to show the addition of the Seasons and Episodes. However, when I press Hot Reload, the app is refreshed with all the correct items. I believe that it is not comparing the nested objects, and I don't know how to allow it to compare them with my code.
pub spec.yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.1
equatable: ^2.0.5
main.dart
import 'package:deeply_nested_objects/bloc/add_to_collection_logic.dart';
import 'package:deeply_nested_objects/bloc/collection_bloc.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CollectionBloc(),
child: const MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return BlocBuilder<CollectionBloc, CollectionState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Deeply nested data and Bloc 8.0.0+'),
),
body: ListView.builder(
itemCount: state.getAllNodes(state).length,
itemBuilder: (context, index) {
var nodes = state.getAllNodes(state)[index];
Color textColor = getColor(nodes);
double distance = getPaddingDistance(nodes);
return Padding(
padding: EdgeInsets.only(left: distance),
child: ListTile(
onTap: () => addToCollectionLogic(nodes.showType, index,
nodes.children.length + 1, context),
leading: Card(
child: Text(nodes.name, style: TextStyle(color: textColor)),
),
),
);
},
),
);
},
);
}
double getPaddingDistance(CollectionState nodes) {
switch (nodes.showType) {
case ShowType.collection:
return 0;
case ShowType.series:
return 20;
case ShowType.season:
return 40;
case ShowType.episode:
return 60;
}
}
Color getColor(CollectionState nodes) {
switch (nodes.showType) {
case ShowType.collection:
return Colors.black;
case ShowType.series:
return Colors.blue;
case ShowType.season:
return Colors.green;
case ShowType.episode:
return Colors.red;
}
}
}
add_collection_logic.dart
import 'package:deeply_nested_objects/bloc/collection_bloc.dart';
import 'package:deeply_nested_objects/bloc/collection_event.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void addToCollectionLogic(
ShowType showType, int index, int count, BuildContext context) {
void passToBloc(String name, ShowType showType) =>
BlocProvider.of<CollectionBloc>(context).add(
AddInfo(
index: index,
child: CollectionState(name: name, showType: showType, children: []),
),
);
switch (showType) {
case ShowType.collection:
passToBloc('Series $count', ShowType.series);
break;
case ShowType.series:
passToBloc('Season $count', ShowType.season);
break;
case ShowType.season:
passToBloc('Episode $count', ShowType.episode);
break;
case ShowType.episode:
break;
}
}
collection_event.dart
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:equatable/equatable.dart';
abstract class CollectionEvents extends Equatable {
#override
List<Object> get props => [];
}
class AddInfo extends CollectionEvents {
AddInfo({required this.index, required this.child});
final int index;
final CollectionState child;
}
collection_bloc.dart
import 'package:deeply_nested_objects/bloc/collection_event.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CollectionBloc extends Bloc<CollectionEvents, CollectionState> {
CollectionBloc() : super(CollectionState.initial()) {
on<AddInfo>((event, emit) {
if (event.child.showType == ShowType.series) {
emit(state.copyWith(children: [...state.children, event.child]));
}
if (event.child.showType == ShowType.season ||
event.child.showType == ShowType.episode) {
//TODO: Here is the problem. This code does not work properly when I extend the CollectionState class to Equatable.
// get the list of all nodes
List<CollectionState> list = state.getAllNodes(state);
// find the parent node while still in the list
CollectionState parent = list[event.index];
// add the child to the parent
parent.children.add(event.child);
// update the state
emit(state.copyWith(children: [...state.children]));
}
});
}
}
collection_state.dart
enum ShowType { collection, series, season, episode }
// 1) where should be Equatable
class CollectionState {
const CollectionState({
required this.name,
required this.children,
required this.showType,
});
final String name;
final List<CollectionState> children;
final ShowType showType;
factory CollectionState.initial() {
return const CollectionState(
name: "Collection",
showType: ShowType.collection,
children: [],
);
}
List<CollectionState> getAllNodes(CollectionState node) {
// empty list to store the result
List<CollectionState> result = [];
// add the current node
result.add(node);
// add the children too
for (CollectionState child in node.children) {
// composite design pattern seek and find
result.addAll(getAllNodes(child));
}
return result;
}
CollectionState copyWith({
String? name,
List<CollectionState>? children,
ShowType? showType,
}) {
return CollectionState(
name: name ?? this.name,
children: children ?? this.children,
showType: showType ?? this.showType,
);
}
}
You have to assign properties in equatable that you want to check equality on.
Example:
class SomeClass extends Equatable {
SomeClass({required this.index});
final int index;
#override
List<Object> get props => [];
}
If I would check some SomeClass(index: 10) == SomeClass(index: 9) it would be true because I didn't say equatable what properties it should look for on == operator
If I update my code to
class SomeClass extends Equatable {
SomeClass({required this.index});
final int index;
#override
List<Object> get props => [index];
}
Now same check would be false, cause it is looking on index property

Flutter/Dart: reaction of ui depending on the inheritance class of an object

I have a class for menu-entries:
abstract class MenuSelection {
MenuSelection({required this.label});
final String label;
}
and two classes that inherit from this class:
///for menu entries with an image
abstract class ImageSelection extends MenuSelection {
ImageSelection({required this.image, required super.label});
final String image;
}
///for menu entries with an icon
abstract class IconSelection extends MenuSelection {
IconSelection({required this.iconData, required super.label});
final IconData iconData;
}
I want to have one DropDownMenu and react depending on the given class:
class _DropDownMenuItem extends StatelessWidget {
const _DropDownMenuItem({super.key, required this.selection});
final MenuSelection selection;
#override
Widget build(BuildContext context) {
return Row(
children: [
if (selection is ImageSelection)
Image.asset(
selection.image,
width: 30,
)
else if (selection is IconSelection)
Icon(
selection.iconData,
size: 30,
)
else
Container(),
const SizedBox(
width: 10,
),
Text(selection.label.tr()),
],
);
}
}
I thought it must work that way because it works for example when used with Bloc and getting the current type of state...
Actually AndroidStudio tells me that class MenuSelection has no image or icon property instead of recognizing that this is ImageSelection or IconSelection.
Can someone explain this behavior?
The implicit type promotion only works for local variables, not for class fields. You can read this answer if you need a detailed explanation and a possible solution.

Freezed class with generic callback

I would like to define a freezed class [https://pub.dev/packages/freezed] with a generic callback.
Freezed class:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'foo.freezed.dart';
#freezed
abstract class Foo<T> with _$Foo {
factory Foo({
// String Function() callBackOne,
String Function(T) callBackTwo,
}) = _Foo;
}
Widget using the Freezed class:
class MyHomePage extends StatelessWidget {
// final fooOne = Foo<int>(callBackOne: () => 'Result: 42');
final fooTwo = Foo<int>(callBackTwo: (value) => 'Result: ${value * 3}');
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(fooTwo.callBackTwo(14)),
),
);
}
}
Error:
lib/foo.freezed.dart:128:26: Error: The return type of the method '_Foo.callBackTwo' is 'String Function(T)', which does not match the return type, 'String Function(dynamic)', of the overridden method, '_$Foo.callBackTwo'.
Change to a subtype of 'String Function(dynamic)'.
String Function(T) get callBackTwo;
^
lib/foo.freezed.dart:31:26: Context: This is the overridden method ('callBackTwo').
String Function(T) get callBackTwo;
^
Do you know what is wrong with my code? Is it a limitation of Freezed? Do you know a workaround?
Thank you!!!
It looks like a flaw in Dart type system. I've encouraged something like that either. I don't know a clean workaround. You can specify not a direct function but a function wrapped into a class with a "strong" method signature. Something like that should work:
#freezed
abstract class Foo<T> with _$Foo {
factory Foo({
Func<T> callBackTwo,
}) = _Foo;
}
class Func<T> {
final String Function(T) _apply;
Func(this._apply) : assert(_apply != null);
String call(T value) {
return _apply(value);
}
}
class MyHomePage extends StatelessWidget {
final fooTwo = Foo<int>(Func<int>((value) => 'Result: ${value * 3}'));
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(fooTwo.callBackTwo(14)),
),
);
}
}
It's not so good, because you are to type more. But we can minimize typing a bit:
#freezed
abstract class Foo<T> with _$Foo {
factory Foo({
Func<T> callBackTwo,
}) = _Foo;
factory Foo.from(String Function(T) arg) {
return Foo<T>(callBackTwo: Func<T>(arg));
}
}
class MyHomePage extends StatelessWidget {
final fooTwo = Foo<int>.from((value) => 'Result: ${value * 3}');
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(fooTwo.callBackTwo(14)),
),
);
}
}

BlocBuilder not updating after change

I have a bloc to manage all the quotations in the application. The quotation class, bloc, and events are given below:
I have a form in which on selecting the text field, I show a list view to the user, and the value of the selected list view is assigned to the bloc and displayed in the text field.
Everything works fine but when I assign the value to the bloc variable and return it back to the form the text field value does update BUT ONLY FOR SINGLE TIME. If I do select some other list option for the same or another field the field value doesn't update.
CAN ANYONE SUGGEST A FIX?
I have a custom textField created as shown below and I'm calling this inside a bloc builder:
BlocBuilder<QuoteBloc, QuoteState>(builder: (context, state) {
if (state is QuoteInitialized) {
return Column(
children: [
BookingFormField(
labelText: "Flying From",
onTap: () => Navigator.push(
context,
AirportCityPlaceSelection.route(
'tq-fb-flight-from',
),
),
controller: TextEditingController(
text: BlocProvider.of<QuoteBloc>(context)
.quote
.flight
.flightFrom,
),
),
BookingFormField(
labelText: "Flying To",
onTap: () {
Navigator.push(
context,
AirportCityPlaceSelection.route(
'tq-fb-flight-to',
),
);
},
controller: TextEditingController(
text: BlocProvider.of<QuoteBloc>(context)
.quote
.flight
.flightTo,
),
),
],
);
}
}),
class BookingFormField extends StatelessWidget {
final Function onTap;
final TextEditingController controller;
final String labelText;
BookingFormField({
#required this.onTap,
#required this.controller,
#required this.labelText,
});
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(
top: 10.0,
bottom: 10.0,
),
child: TextField(
controller: controller,
readOnly: true,
onTap: () => onTap(),
style: Theme.of(context).textTheme.bodyText2.copyWith(
fontSize: 13.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryColor,
),
);
}
}
And this is how I'm updating the value in the list view which is a new screen:
BlocProvider.of<QuoteBloc>(context).quote.flight.flightFrom = value;
BlocProvider.of<QuoteBloc>(context).add(QuoteUpdated());
Navigator.pop(context);
Quote Class:-
part 'flight.dart';
part 'car.dart';
part 'cruise.dart';
part 'hotel.dart';
part 'visa.dart';
part 'insurance.dart';
part 'transfer.dart';
class Quote {
String name;
String contactNumber;
String email;
Flight flight;
Car car;
Hotel hotel;
Cruise cruise;
Transfer transfer;
Visa visa;
Insurance insurance;
// Constructors & other functions
}
The events related to the quote bloc are:
abstract class QuoteEvent extends Equatable {
List<Object> get props => [];
}
class QuoteUpdated extends QuoteEvent {
List<Object> get props => [];
}
The quote State is
abstract class QuoteState extends Equatable {
List<Object> get props => [];
}
class QuoteInitialized extends QuoteState {
final Quote quote;
QuoteInitialized({
#required this.quote,
});
List<Object> get props => [this.quote];
}
class QuoteSubmissionInProgress extends QuoteState {}
class QuoteSubmissionSuccessful extends QuoteState {}
class QuoteSubmissionFailed extends QuoteState {}
Quote Bloc:
class QuoteBloc extends Bloc<QuoteEvent, QuoteState> {
final Quote quote;
QuoteBloc(Quote quote)
: assert(quote != null),
this.quote = quote,
super(QuoteInitialized(quote: quote));
#override
Stream<QuoteState> mapEventToState(QuoteEvent event) async* {
if (event is QuoteUpdated) {
yield QuoteInitialized(quote: this.quote);
}
}
}
Don't update state in UI LAYER (send event to bloc)
Try to remove equatable in QuoteState or Add Equatable to Quote class
A guess is that the state is considered to be the same, meaning that the following times you expect updated fields you actually didn't get a new state. Have you verified that you get a new yielded state in the BlocBuilder?
My guess is based on two things. Firstly, that symptom could manifest in that way. Secondly I don't see methods in the Quote class that allow for equals comparison (maybe you have it where you commented out code).
I had a similar problem which gave me a headache. I was using a cubit and it won't display a progress bar because the loading state was not set. Since bloc extends cubit you might have the same problem. I had to put a future.delayed before emitting the SearchLoading() state. After this change, the state was set and the progress bar was shown. I had this problem in the debug mode of an Android app as well as in the release build.
class SearchCubit extends Cubit<SearchState> {
final ClubRepository _clubRepository = ClubRepository();
final log = getLogger("SearchCubit");
SearchCubit() : super(SearchInitial());
Future<void> getClubs() async {
try {
log.d("Fetch clubs");
await Future.delayed(Duration(microseconds: 1));
emit(SearchLoading());
final List<Club> clubs = await _clubRepository.fetch();
await Future.delayed(Duration(seconds: 2));
emit(SearchLoaded(clubs));
} catch (err, stacktrace) {
emit(SearchError("Retrieving data from API failed!"));
}
}
}
I'm guessing this is happening because your Quote class does not extend Equatable. Please refer to the FAQs for more information 👍