I created an Animal More Button that has 2 options. The first option is eat which applies to all animals and the second option only applies to a specific animal, in this example is bark. Clicking eat will print eat in the console as expected, but clicking bark results in this error in the console.
After debugging, option.callback(context, animalModel) inside _onSelected(BuildContext context, AnimalMoreButtonOption option returns this error.
Console
errors.dart:251 Uncaught (in promise) Error: Expected a value of type '(BuildContext, AnimalModelType<Animal>) => void', but got one of type '(BuildContext, DogsViewModel) => void'
at Object.throw_ [as throw] (errors.dart:251:49)
at Object.castError (errors.dart:84:3)
at Object.cast [as as] (operations.dart:452:10)
at dart.FunctionType.new.as (types.dart:829:12)
at _onSelected (main.dart:62:20)
at _onSelected.next (<anonymous>)
at runBody (async_patch.dart:84:54)
at Object._async [as async] (async_patch.dart:123:5)
at AnimalMoreButton.new.[_onSelected] (main.dart:60:19)
at main.dart:69:31
at popup_menu.dart:1144:28
at _RootZone.runUnary (zone.dart:1685:54)
at _FutureListener.then.handleValue (future_impl.dart:159:18)
at handleValueCallback (future_impl.dart:766:44)
at Function._propagateToListeners (future_impl.dart:795:13)
at _Future.new.[_completeWithValue] (future_impl.dart:601:5)
at async._AsyncCallbackEntry.new.callback (future_impl.dart:639:7)
at Object._microtaskLoop (schedule_microtask.dart:40:11)
at _startMicrotaskLoop (schedule_microtask.dart:49:5)
at async_patch.dart:166:15
I don't understand why the type is wrong in the callback. Because DogsViewModel implements AnimalModelType<Dog> and Dog extends Animal. AnimalModelType is a generic class AnimalModelType<TAnimal extends Animal>.
AnimalMoreButton is a class that has option eat for all animals, it takes additionalOptions param for specific animal options. In this case MenuButton<AnimalMoreButtonOption> has 2 options of type MenuOption<AnimalMoreButtonOption<AnimalModelType>> and MenuOption<AnimalMoreButtonOption<DogsViewModel>>.
@override
Widget build(BuildContext context) {
return MenuButton<AnimalMoreButtonOption>(
label: 'more',
onSelected: (option) => _onSelected(context, option),
options: [
MenuOption<AnimalMoreButtonOption<AnimalModelType>>(
value: AnimalMoreButtonOption.eat,
label: 'eat',
),
// ...additionalOptions,
// MenuOption<AnimalMoreButtonOption<DogsViewModel>>(
// value:
// AnimalMoreButtonOption((context, model) => _bark(context, model)),
// label: 'bark',
// ),
],
);
}
main.dart
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Animal More Button',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Animal More Button'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
final model = DogsViewModel();
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Provider(
create: (BuildContext context) => model,
child: Center(
child: AnimalMoreButton<DogsViewModel>(
additionalOptions: model.getAdditionalMoreButtonOptions(context),
),
),
),
);
}
}
class AnimalMoreButton<T extends AnimalModelType> extends StatelessWidget {
const AnimalMoreButton({this.additionalOptions = const [], Key? key})
: super(key: key);
/// Additional options to display in the popup menu
final List<MenuOption<AnimalMoreButtonOption>> additionalOptions;
void _onSelected(BuildContext context, AnimalMoreButtonOption option) async {
final animalModel = context.read<T>();
option.callback(context, animalModel);
}
@override
Widget build(BuildContext context) {
return MenuButton<AnimalMoreButtonOption>(
label: 'more',
onSelected: (option) => _onSelected(context, option),
options: [
MenuOption<AnimalMoreButtonOption>(
value: AnimalMoreButtonOption.eat,
label: 'eat',
),
...additionalOptions,
],
);
}
}
class AnimalMoreButtonOption<T extends AnimalModelType> {
const AnimalMoreButtonOption(this.callback);
final void Function(BuildContext context, T animalModel) callback;
static final eat = AnimalMoreButtonOption((context, animalModel) {
print('eat');
});
}
abstract class AnimalModelType<TAnimal extends Animal> {
AnimalModelType();
List<MenuOption<AnimalMoreButtonOption<AnimalModelType>>>
getAdditionalMoreButtonOptions(BuildContext context) => [];
}
class Animal {
const Animal({
required this.name,
});
final String name;
}
class Dog extends Animal {
const Dog({
required String name,
}) : super(name: name);
}
class DogsViewModel implements AnimalModelType<Dog> {
@override
List<MenuOption<AnimalMoreButtonOption<DogsViewModel>>>
getAdditionalMoreButtonOptions(BuildContext context) {
return [
MenuOption(
value:
AnimalMoreButtonOption((context, model) => _bark(context, model)),
label: 'bark',
),
];
}
void _bark(BuildContext context, DogsViewModel model) {
print('bark');
}
}
class MenuOption<T> extends Equatable {
const MenuOption({
required this.value,
required this.label,
});
final T value;
final String label;
@override
List<T> get props => [value];
}
class MenuButton<T> extends StatelessWidget {
const MenuButton({
required this.label,
required this.options,
this.onSelected,
Key? key,
}) : super(key: key);
final String label;
final List<MenuOption<T>> options;
final void Function(T)? onSelected;
@override
Widget build(BuildContext context) {
return PopupMenuButton<T>(
onSelected: onSelected,
itemBuilder: (_) => options
.map(
(option) => PopupMenuItem(
value: option.value,
child: Builder(builder: (buildContext) {
return Text(
option.label,
);
}),
),
)
.toList(),
padding: EdgeInsets.zero,
child: Text(
label,
),
);
}
}