17

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();
  }
}
Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
Ciprian
  • 3,066
  • 9
  • 62
  • 98

4 Answers4

13

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();
      }
    }
    
Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • This solution is fine. I thought that the OP only wanted to load once. In that case a `ChangeNotifier` does not seem appropriate and using `Future.microtask` to escape the calling `setState` from `build` limitation seems like a hack. – creativecreatorormaybenot Jun 06 '19 at 15:51
1

I have encountered a problem similar to yours when using the provider. My solution is to add WidgetsBinding.instance.addPostFrameCallback() when getting the data.

Jansid
  • 11
  • 1
1

Simply remove the notifyListeners(); from the code. I encountered this error and that was what I did to resolve the issue.

Emmanuel
  • 31
  • 1
  • 4
0

There is another approach to handle this by using StreamSubscription on Future. It will take more boilerplate code but it can help to make sure that other logic can be separated into widget building stuff in order to prevent side effects.

Future? _dataFuture;
StreamSubscription? _dataSubscription;
FutureBuilder(
    future: _dataFuture,
    builder: (context, snapshot) {
        switch (snapshot.connectionState) {
            // removed data updating logic from here
            // context.read<DataProvider>().updateData(newData: snapshot.data);
            case ConnectionState.done:
                return Text(
                    snapshot.data,
                    style: Theme.of(context).textTheme.headline5,
                );
        },
    },
),
floatingActionButton: FloatingActionButton(
    onPressed: () => _getData(),
    child: const Icon(Icons.adb),
),
_getData() {
  _dataSubscription?.cancel();
  setState(() {
    _dataFuture = Util.getText();
    _dataSubscription = _dataFuture?.asStream().listen((data) {
      if (!mounted) return;
      context.read<DataProvider>().updateData(newData: data);
    });
  });
}

Check out the full sample code for this from here.

Huy Nguyen
  • 1,032
  • 14
  • 38