Flutter - how to fetch data correctly with ChangeNotifier

Viewed 405

I want to fetch data about weather stats for entire app once after app is runned.

My ChangeNotifier:

class WeatherStatsNotifier extends ChangeNotifier {
  WeatherService weatherService;
  Future<WeatherStats> _stats;

  WeatherStatsNotifier({this.weatherService});

  Future<WeatherStats> get stats => _stats;

  void refreshStats() {
    _stats = weatherService.fetchWeatherStats();
    notifyListeners(); // it works when I remove this line
  }
}

My main file:

final WeatherService weatherService = new WeatherService();

void main() => runApp(ChangeNotifierProvider(
    create: (context) => WeatherStatsNotifier(weatherService: weatherService),
    child: App()));

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  void didChangeDependencies() {
    Provider.of<WeatherStatsNotifier>(context, listen: false).refreshStats();
    super.didChangeDependencies();
  }

  //... build method
}

and somewhere deeper:

@override
  Widget build(BuildContext context) {
    return Consumer<WeatherStatsNotifier>(
      builder: (context, value, child) {
        return FutureBuilder<WeatherStats>(
          future: value.stats,
          builder: (context, snapshot) {
            return _buildForSnapshot(snapshot);
          },
        );
      },
    );
  }

Finally I get an error:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building ChangeNotifierProvider<WeatherStatsNotifier>(message: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4195 pos 12: '!_dirty': is not true.\nSee also: https://flutter.dev/docs/testing/errors, dirty, renderObject: RenderErrorBox#c84e8 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE DETACHED):
'package:flutter/src/widgets/framework.dart': Failed assertion: line 4195 pos 12: '!_dirty': is not true.

How to do it in correct way? Am I able to use context inside initState or didChangeDependencies or not?

3 Answers

When dependencies change, that is WeatherStatsNotifier, didChangeDependencies is called. Widget is already considered dirty and build is in progress (there can be multiple listeners).

However your refreshStats is notifing listeners synchronously which would mean that listeners should be build again, but they are already building. That could lead to inconsistency in the frame so that's why it is an error. That's also why removing this notifyListeners(); is "fixing" the problem.

The way to fix it is to execute the change async either by addPostFrameCallback or Future.microtask.

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    Future.microtask(() {
      Provider.of<WeatherStatsNotifier>(context, listen: false).refreshStats();
    });

  }

Note that if you have multiple dependencies you probably should check if instances have changed in didChangeDependencies.

Here is my best practice how to fetch data using value notifier

  1. Define the value notifier with the type data
ex:
class _AppState extends State<App> {
ValueNotifier<String> _weatherStatsNotifier;
  1. Add to init state for default value (To handle null value) and yes you can use context too if needed
@override
void initState() {
super.initState();
_weatherStatsNotifier = ValueNotifier(emtpyString);
  1. Then, Change the value if the data already fetch
_weatherStatsNotifier.value = weatherService;

or in my case I'm usually using the value notifier from listen state

_weatherBloc.listen(
        (state) {
          if (state is weatherLoadedState) {
            _weatherStatsNotifier.value = state.weather;
          }
        },
      );

4.Use the data as the return of the data ex String

ValueListenableBuilder<String>(
valueListenable: _weatherStatsNotifier,
builder: (context, value, _) => NextScreen(
                                Weather: value,
                                ),
                              ),

Consider wrapping your method inside SchedulerBinding.instance.addPostFrameCallback so the method gets called in the next frame and not just when the widget is being rebuilt

class _AppState extends State<App> {
  @override
  void didChangeDependencies() {
    SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
    Provider.of<WeatherStatsNotifier>(context, listen: false).refreshStats();
}
    super.didChangeDependencies();
  }


    });



Related