3

enter image description here

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?

Anthony O
  • 622
  • 7
  • 26
  • Looks like you'll need an explicit cast, `factory Dog fromAnimal(Animal animal);` https://stackoverflow.com/questions/67352381/dart-downcasting – Rohan Thacker Jun 22 '21 at 04:49
  • I did try doing an explicit case using the **as** keyword but that threw the error. I have a working solution below, although its more verbose than I would like. – Anthony O Jun 22 '21 at 05:49

1 Answers1

0

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 :)

Anthony O
  • 622
  • 7
  • 26