Search code examples
flutterdartflutter-provider

How to set a loading indicator while FutureProvider is not done


I'm using FutureProvider to fetch data from a local db with SQflite, and then render a graph in the Consumer child. However, when loading the app, during a brief period an error is shown :

The following StateError was thrown building Consumer<List<Map<String, dynamic>>>(dirty,
                    dependencies: [_InheritedProviderScope<List<Map<String, dynamic>>>]):
                    Bad state: No element

After the graph is rendered fine.

How can I catch this loading state so the error disappears and I can show a CircularProgressIndicator() ?

Parent

 FutureProvider<List<Map<String, dynamic>>>(
        create: (context) {
          return RecordsDatabase.instance.getRecords();
        },
        catchError: (context, error) {
          print("error: ${error.toString()}");
          return [];
        },
        initialData: [],
        child: HomeCustom(),
      )

Child

@override
  Widget build(BuildContext context) {
    return Consumer<List<Map<String, dynamic>>>(
        builder: (context, records, child) {
      GraphState graph =GraphState(records: records, context: context);

      return ChangeNotifierProvider<GraphState>(
        create: (_) => graph,
        child: Scaffold(
          backgroundColor: Colors.black,
          body: Stack(children: [
            Center(
                child: graph.records.isEmpty
                        ? Text(
                            'No Records',
                            style: TextStyle(color: Colors.white, fontSize: 24),
                          )
                        : MyGraph()),
            Align(
              alignment: Alignment.bottomRight,
              child: Padding(
                padding: const EdgeInsets.only(right: 30, bottom: 50),
                child: FloatingActionButton(
                  child: Icon(Icons.add),
                  onPressed: _setVisible,
                ),
              ),
            )
          ]),
        ),
      );
    });
  }
}


Solution

  • In the Consumer, check the records value first then return the appropriate widget.

    Sample...

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: FutureProvider<List<Map<String, dynamic>>?>(
            create: (_) => _getRecords(),
            initialData: null,
            catchError: (_, __) => <Map<String, dynamic>>[
              {'error': 'Something went wrong'}
            ],
            child: HomePage(),
          ),
        );
      }
    
      Future<List<Map<String, dynamic>>> _getRecords() async {
        final bool isError = false; // set to "true" to check error case
    
        await Future<void>.delayed(const Duration(seconds: 5));
    
        if (isError) {
          throw Exception();
        }
    
        return <Map<String, dynamic>>[
          <String, int>{'item': 1},
          <String, String>{'itemTxt': 'one'},
        ];
      }
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Consumer<List<Map<String, dynamic>>?>(
              builder: (_, List<Map<String, dynamic>>? records, __) {
                if (records == null) {
                  return const CircularProgressIndicator();
                } else if (records.isNotEmpty &&
                    records.first.containsKey('error')) {
                  return Text(records.first['error'] as String);
                }
    
                return Text(records.toString());
              },
            ),
          ),
        );
      }
    }