Search code examples
flutterproviderflutter-state

How can I build a list of elements from a provider that fetches the data from somewhere and refereshes when the provider changes?


Hello I'm having a lot of trouble understanding how to use providers when fetching data.

Lets say I have an http service that fetches the data and updates a list:

my_provider.dart:

class InventoryProvider extends ChangeNotifier {
  List<Item> _items = [];
  List<Item> get items => List.unmodifiable(_items);

  Future<List<Item>> fetchAllItems() async {
    _items = await ItemService().fetchAllItems();
    notifyListeners();
    return items;
  }
}

Now I want to build my list. The only solution I have found to work is the following:

my_screen:

class InventoryScreen extends StatefulWidget {
  static const routeName = '/';

  const InventoryScreen({
    super.key,
  });

  @override
  State<InventoryScreen> createState() => _InventoryScreenState();
}

class _InventoryScreenState extends State<InventoryScreen> {
  final List<Item> _items = [];
  bool _isLoading = false;
  String _errorMessage = '';

  /// Fetch items
  @override
  void initState() {
    super.initState();
    _isLoading = true;
    final itemProvider = Provider.of<InventoryProvider>(context, listen: false);
    itemProvider.fetchAllItems().then((posts) {
      setState(() {
        for (Item item in posts) {
          _items.add(item);
        }
        _isLoading = false;
      });
    }).catchError((e) {
      setState(() {
        _errorMessage = e.message;
        _isLoading = false;
      });
    });
  }
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
        ...
       body: _isLoading
            ? const Center(child: CircularProgressIndicator())
            : _errorMessage.isNotEmpty
                ? Center(child: Text(_errorMessage))
                : _items.isEmpty
                    ? const Center(child: Text('You do not have any item yet.'))
                    : ListView.builder(
                        itemCount: _items.length,
                        itemBuilder: (BuildContext context, int index) {
                          final item = _items[index];

                          return ListTile(

So this works for the first fetch as:

  • It displays something (the progress indicator) while the data is being fetched.
  • I obtain all the data from the provider.

It does not work for:

  • Refreshing the list in case the provider changes (For example, if when you access to the item details you can delete one item of the list and then pop back to the list).

What I have tried:

  • Remove the listen: False. Triggers:
Exception has occurred.
FlutterError (dependOnInheritedWidgetOfExactType<_InheritedProviderScope<InventoryProvider?>>() or dependOnInheritedElement() was called before _InventoryScreenState.initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.)
  • Change the initState for didChangeDependencies with listen false: Enters an infinite loop of requests.

Any help on how should this be done?


Solution

  • In your ImageProvider do this changes:

    class InventoryProvider extends ChangeNotifier {
      List<Item> items = [];
    
      Future<List<Item>> fetchAllItems() async {
        items.addAll(await ItemService().fetchAllItems());
        notifyListeners();
        return items;
      }
    }
    

    In your initState do this changes:

    @override
      void initState() {
        super.initState();
        _isLoading = true;
        final itemProvider = Provider.of<InventoryProvider>(context, listen: false);
        itemProvider.fetchAllItems().whenComplete(() {
          setState(() {
            _isLoading = false;
          });
        }).catchError((e) {
          setState(() {
            _errorMessage = e.message;
          });
        });
      }
    

    Wrap your scaffold with selector like this:

    @override
      Widget build(BuildContext context) {
        return Selector<InventoryProvider, List<Item>>(
          selector: (_, provider) => provider.items,
          builder: (_, items, __) {
            return Scaffold(...);
          },
        );
      }