Setting provider value in FutureBuilder
Asked Answered
T

4

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();
  }
}
Teletypewriter answered 29/5, 2019 at 11:2 Comment(6)
and where are you using setter for myList?Biffin
In the FutureBuilder ... if (snapshot.hasData) { // appState.myList = snapshot.data;Teletypewriter
and why not inside getPosts?Biffin
yeah ... that would be even better ... but can I have final appState = Provider.of<MySchedule>(context); in there? I don't have context ... I never really understood what context does. I'm new to Flutter. Please post an answer.Teletypewriter
i dont know, i have no experience with Provider classBiffin
That would help :) Thanks for replying though.Teletypewriter
E
14

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();
      }
    }
    
Example answered 6/6, 2019 at 15:37 Comment(1)
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.Dall
G
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.

Grits answered 23/8, 2019 at 1:53 Comment(1)
please provide an exampleTeishateixeira
N
0

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

Noiseless answered 6/11, 2020 at 8:44 Comment(2)
How do you fix a code? By removing the code! Genius.Nastassia
T H A N K Y O U <3Ocrea
C
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.

Constructive answered 13/12, 2022 at 17:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.