Expected a value of type '(BuildContext, AnimalModelType<Animal>) => void', but got one of type '(BuildContext, DogsViewModel) => void'

Viewed 50

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,
          ),
    );
  }
}

0 Answers
Related