Search code examples
flutterdartflutter-futurebuildersembast

Using a FutureBuilder but then modifying a local value in Flutter


I'm trying to get my head around how I should manage reading data from a database in Flutter (or any API/backend).

Without a database and just using state that's local to the widget I'd expect to write something similar to the following. I am printing out a list of strings in a ListView and adding a new one when a FloatingActionButton is pressed.

class _ListScreenState extends State<ListScreen> {
  var _myStrings = ["string1", "string2"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: _myStrings.length,
        itemBuilder: (BuildContext context, int index) {
          return Text(_myStrings[index]);
        }
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => {
            setState(() {
              _myStrings.add("another string");
            });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

However, when reading from a database, I've extracted the database logic behind a repository class and so I'm using a FutureBuilder.

class _ListScreenState extends State<ListScreen> {
  final _repo = FooRepository();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder(
          future: _repo.getAll(),
          builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
            if (snapshot.data == null) {
              return const Center(child: Text("Loading..."));
            }
            return ListView<String, String>(
                elements: snapshot.data,
                itemBuilder: (context, element) {
                  return Text(element);
                }
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => {
          _repo.add("newstring");
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

This seems to go against the idea of the flutter widget being reactive to changes in its local state. Am I doing something wrong here? I don't know whether calling setState() will cause the FutureBuilder to redraw but it also seems wasteful to re-get all the records from the database when one is added. Should/can I hold a copy of the strings locally to the widget and add to both the database but also the local list?


Solution

  • When using a FutureBuilder you should not need to use setState, as the actual update of the state, based on the Future's response, will be managed by the FutureBuilder itself.

    The problem here is that FutureBuilder is used when you want to retrieve some data once, but from your example, you would like that once you press your button and a new String is added to the Database, that the widget displays reactively the new data. In this case probably you want to use a StreamBuilder, which is very similiar to a FutureBuilder, but it's used when you want to display some data that can change over time.

    With your example, using a FutureBuilder, when you add some data to the Database the only way to get the new data would be to refetch it via the _repo.getAll() method. This can work if it's what you want, you simply can force a rebuild once the button is pressed, and the future will run again.

    To answer your final question, if you want you can manage your state locally, in this case, I would suggest to refactor your code, and add some state management logic, for example a simple Provider. There you can initialize your data by calling once _repo.getAll(), and in your Widget, you would not use anymore a FutureBuilder or StreamBuilder, but you would listen to the changes coming from the Provider, via a Consumer, then when your button is pressed, the Provider can add that piece of data to the object and your UI would see immediately the changes. I can provide you a simple example:

    your provider, that manages the state:

    class YourDataProvider extends ChangeNotifier {
          bool isLoading = true;
          List<String> myData = [];
        
          Future<void> getAll() async {
            isLoading = true;
            notifyListeners();
        
            myData = await yourAsyncCall();
        
            isLoading = false;
            notifyListeners();
          }
        
          Future<void> add(String value) async {
            isLoading = true;
            notifyListeners();
        
            await yourAddAsyncCall(value);
        
            myData.add(value);
        
            notifyListeners();
          }
        }
    

    your UI widget

    .....
      @override
      void initState() {
        context.read<YourDataProvider>.getAll();
        super.initState();
      }
    
    class _ListScreenState extends State<ListScreen> {
    
      @override
      Widget build(BuildContext context) {
        YourDataProvider provider = context.watch<YourDataProvider>;
    
        return Scaffold(
          body: provider.isLoading
          ? Center(child: CircularProgressIndicator())
          : ListView.builder(
            itemCount: provider.myData.length,
            itemBuilder: (context, index) => Text(provider.myData[index]),
          )
        ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => provider.add("newstring"),
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    

    As you can see, the Provider will fetch once the data, and then it will hold the state, and once a new String is added, it will only submit it to the backend, but it won't be refetching all the data again.