Setting provider value in FutureBuilder

Ciprian picture Ciprian · May 29, 2019 · Viewed 12k times · Source

I have a widget that makes a request to an api which returns a map. What I would like to do is not make the same request every time the widget is loaded and save the list to appState.myList. But. when I do this appState.myList = snapshot.data; in the FutureBuilder, I get the following error:

flutter: ══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY ╞════════════════════════════════════════════════════════
flutter: The following assertion was thrown while dispatching notifications for MySchedule:
flutter: setState() or markNeedsBuild() called during build.
flutter: This ChangeNotifierProvider<MySchedule> widget cannot be marked as needing to build because the
flutter: framework is already in the process of building widgets. A widget can be marked as needing to be
flutter: built during the build phase only if one of its ancestors is currently building. ...

sun.dart file:

class Sun extends StatelessWidget {
  Widget build(BuildContext context) {
    final appState = Provider.of<MySchedule>(context);
    var db = PostDB();

    Widget listBuild(appState) {
      final list = appState.myList;
      return ListView.builder(
        itemCount: list.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(list[index].title));
        },
      );
    }

    Widget futureBuild(appState) {
      return FutureBuilder(
        future: db.getPosts(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.hasData) {
            // appState.myList = snapshot.data;
            return ListView.builder(
              itemCount: snapshot.data.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(snapshot.data[index].title));
              },
            );
          } else if (snapshot.hasError) {
            return Text("${snapshot.error}");
          }
          return Center(
            child: CircularProgressIndicator(),
          );
        },
      );
    }

    return Scaffold(
        body: appState.myList != null
            ? listBuild(appState)
            : futureBuild(appState));
  }
}

postService.dart file:

class PostDB {
  var isLoading = false;

  Future<List<Postmodel>> getPosts() async {
    isLoading = true;
    final response =
        await http.get("https://jsonplaceholder.typicode.com/posts");

    if (response.statusCode == 200) {
      isLoading = false;
      return (json.decode(response.body) as List)
          .map((data) => Postmodel.fromJson(data))
          .toList();
    } else {
      throw Exception('Failed to load posts');
    }
  }
}

I understand that the myList calls notifyListeners() and that's what causes the error. Hope I got that right. If so, how do I set appState.myList and use in the app without getting the above error?

import 'package:flutter/foundation.dart';
import 'package:myflutter/models/post-model.dart';

class MySchedule with ChangeNotifier {
  List<Postmodel> _myList;

  List<Postmodel> get myList => _myList;

  set myList(List<Postmodel> newValue) {
    _myList = newValue;
    notifyListeners();
  }
}

Answer

R&#233;mi Rousselet picture Rémi Rousselet · Jun 6, 2019

That exception arises because you are synchronously modifying a widget from its descendants.

This is bad, as it could lead to an inconsistent widget tree. Some widget. may be built widgets using the value before mutation, while others may be using the mutated value.

The solution is to remove the inconsistency. Using ChangeNotifierProvider, there are usually 2 scenarios:

  • The mutation performed on your ChangeNotifier is always done within the same build than the one that created your ChangeNotifier.

    In that case, you can just do the call directly from the constructor of your ChangeNotifier:

    class MyNotifier with ChangeNotifier {
      MyNotifier() {
        // TODO: start some request
      }
    }
    
  • The change performed can happen "lazily" (typically after changing page).

    In that case, you should wrap your mutation in an addPostFrameCallback or a Future.microtask:

    class Example extends StatefulWidget {
      @override
      _ExampleState createState() => _ExampleState();
    }
    
    class _ExampleState extends State<Example> {
      MyNotifier notifier;
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        final notifier = Provider.of<MyNotifier>(context);
    
        if (this.notifier != notifier) {
          this.notifier = notifier;
          Future.microtask(() => notifier.doSomeHttpCall());
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }