skip to Main Content

I’m encountering a TypeError with the message:

Type '(Job) => ListTile' is not a subtype of type '(Model) => Widget'

in my Flutter app when trying to build a widget by passing a callback. The callback type is defined as:

final Widget Function(T) listItemBuilder;

in a class:

class ListPageBuilder<T extends Model> extends StatefulWidget

In the calling code, I instantiated ListPageBuilder with:

ListPageBuilder(
  listItemBuilder: (Job item) {
    return ListTile(
      key: Key(item.pk!),
      title: Text(item.title),
      subtitle: Text(item.description),
    );
  },
);

where Job inherits from Model. I expected this to work since Job is a subclass of Model, and ListTile is a Widget.

Here is full code

ListPageBuilder implementation


class ListPageBuilder<T extends Model> extends StatefulWidget {
  final bool canView;
  final bool canAdd;
  final String modelVerboseName;
  final Repository repository;
  final void Function()? onItemTap;
  final Widget Function(T) listItemBuilder;

  const ListPageBuilder({
    super.key,
    required this.modelVerboseName,
    required this.canView,
    required this.canAdd,
    required this.repository,
    required this.listItemBuilder,
    this.onItemTap,
  });
  @override
  State<ListPageBuilder> createState() => _ListPageBuilderState();
}

class _ListPageBuilderState extends State<ListPageBuilder> {
  @override
  void initState() {
    super.initState();
    fetchItems();
  }

  var items = [];
  int? nextPage = 1;
  bool _isLoading = true;
  final _scrollController = ScrollController();
  final _searchController = TextEditingController();

  String? getSearchTerm() {
    return _searchController.text.length >= 3 ? _searchController.text : null;
  }

  Map<String, dynamic> getFilters() {
    var filters = <String, dynamic>{"page": nextPage};
    var searchTerm = getSearchTerm();
    if (searchTerm != null) {
      filters["search"] = searchTerm;
    }
    return filters;
  }

  void fetchItems() async {
    if (nextPage == null) return;
    try {
      var response = await widget.repository.list(getFilters());
      var items = response.results;
      nextPage = response.next != null ? getPage(response.next!) : null;

      _isLoading = false;
      setState(() {
        this.items.addAll(items);
      });
    } on HttpException catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    _scrollController.addListener(() {
      var nextPageTriggerLimit =
          0.75 * _scrollController.position.maxScrollExtent;

      if (_scrollController.position.pixels > nextPageTriggerLimit &&
          !_isLoading) {
        _isLoading = true;
        fetchItems();
      }
    });

    return Scaffold(
      bottomNavigationBar: const AppNavigationBar(),
      floatingActionButton: widget.canAdd
          ? FloatingActionButton(
              onPressed: () {
                Navigator.pushNamed(context, "/customers/create");
              },
              child: const Icon(Icons.add),
            )
          : null,
      body: SafeArea(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: widget.canView
                ? Column(
                    children: [
                      TextFormField(
                        decoration: InputDecoration(
                          hintText: "Rechercher un ${widget.modelVerboseName}",
                          suffixIcon: const Icon(Icons.search),
                        ),
                        controller: _searchController,
                        onChanged: (value) {
                          if (value.isEmpty || value.length >= 3) {
                            setState(() {
                              items = [];
                              nextPage = 1;
                              fetchItems();
                            });
                          }
                        },
                      ),
                      (items.isEmpty && _isLoading)
                          ? const SizedBox(
                              width: 50,
                              height: 50,
                              child: CircularProgressIndicator(),
                            )
                          : Expanded(
                              child: ListView.separated(
                                controller: _scrollController,
                                itemCount: items.length,
                                itemBuilder: (context, index) {
                                  var item = items[index];
                                  return widget.listItemBuilder(item);
                                },
                                separatorBuilder: (context, index) =>
                                    const Divider(),
                              ),
                            )
                    ],
                  )
                : const Center(
                    child: Text(
                      "Vous n'avez pas la permission de consulter cette page",
                    ),
                  ),
          ),
        ),
      ),
    );
  }
}

and calling code is

class JobListPage extends StatelessWidget {
  const JobListPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ListPageBuilder<Job>(
      canView: true,
      canAdd: true,
      repository: jobRepository,
      modelVerboseName: "job",
      listItemBuilder: (Job item) {
        return ListTile(
          key: Key(item.pk!),
          title: Text(item.title),
          subtitle: Text(item.description),
        );
      },
    );
  }
}

How can I resolve this issue?

2

Answers


  1. Chosen as BEST ANSWER

    I'm able to make it works by writing

    class JobListPage extends StatelessWidget {
      const JobListPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return ListPageBuilder<Job>(
          canView: true,
          canAdd: true,
          repository: jobRepository,
          modelVerboseName: "job",
          listItemBuilder: (Model item) {
            Job job = item as Job;
            return ListTile(
              key: Key(job.pk!),
              title: Text(job.title),
              subtitle: Text(job.description),
            );
          },
        );
      }
    }
    

    It is not clean but it works and I'm still looking for and better solution.


  2. The issue I see here is that, while ListPageBuilder has a generic type definition, the underlying state class _ListPageBuilderState does not. The items variable in the state class has a type of dynamic since you neither explicitly provide an initial list nor specify a type. I suspect the issue will be resolved once you correctly define a generic type on the state class and data list variable.

    To resolve the issue, set the explicit type of the items variable to T of the state class with generic <T extends Model>:

    
    ///...
    
    class ListPageBuilder<T extends Model> extends StatefulWidget {
      const ListPageBuilder({
        super.key,
        ///...
        required this.listItemBuilder,
      });
    
      final Widget Function(T) listItemBuilder;
      ///...
    
      @override
      State<ListPageBuilder<T>> createState() => _ListPageBuilderState<T>();
    }
    
    class _ListPageBuilderState<T extends Model> extends State<ListPageBuilder<T>> {
      ///...
    
      final items = <T>[];
      
      ///...
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search