Search code examples
flutterdartflutter-layoutflutter-futurebuilder

Flutter FutureBuilder refresh when TextField value changes


The _futureData is to used for the FutureBuilder after retrieving value from the _loadPhobias() function.

entry_screen.dart

Future _futureData;
final TextEditingController _textEditingController = TextEditingController();

_loadPhobias() function does not seem to have any problem.

entry_screen.dart

Future<List<String>> _loadPhobias() async =>
    await rootBundle.loadString('assets/phobias.txt').then((phobias) {
    List _listOfAllPhobias = [];
    List<String> _listOfSortedPhobias = [];
    _textEditingController.addListener(() {
      ...
    }); 
    return _listOfSortedPhobias;
});

@override
void initState() {
super.initState();
    _futureData = _loadPhobias();
}

@override
Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: TextField(
        // When the value is changed, the value returned from the _loadPhobias will also change. So I want the FutureBuilder to be rebuilt.
        onChanged: (text) { setState(() => _futureData =  _loadPhobias()) },
        ),
    ),
    body: FutureBuilder(
        future: _futureData,
        builder: (context, snapshot) {
            return snapshot.hasData
                ? ListView.builder(
                    itemCount: snapshot.data.length,
                    itemBuilder: (context, index) => Column(
                            children: <Widget>[
                                PhobiasCard(sentence: snapshot.data[index]),
                            )
                        ],
                    ))
                    : Center(
                        child: CircularProgressIndicator(),
                    );
                },
            ),
        ),
    );
}

This is the error that I got:

FlutterError (setState() callback argument returned a Future.

The setState() method on _EntryScreenState#51168 was called with a closure or method that returned a Future. Maybe it is marked as "async".

Instead of performing asynchronous work inside a call to setState(), first execute the work (without updating the widget state), and then synchronously update the state inside a call to setState().)


Solution

  • The first thing to note, you mentioned that you want to rebuild your app every time there's a change in the text. For that to happen, you should use StreamBuilder instead. FutureBuilder is meant to be consumed once, it's like a fire and forget event or Promise in JavaScript.

    Here's to a good comparison betweenStreamBuilder vs FutureBuilder.

    This is how you would refactor your code to use StreamBuilder.

    main.dart

    import 'dart:async';
    import 'dart:convert';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyAppScreen(),
        );
      }
    }
    
    class MyAppScreen extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return MyAppScreenState();
      }
    }
    
    class MyAppScreenState extends State<MyAppScreen> {
      StreamController<List<String>> _phobiasStream;
    
      final TextEditingController _textEditingController = TextEditingController();
    
      void _loadPhobias() async =>
          await rootBundle.loadString('lib/phobia.txt').then((phobias) {
            List<String> _listOfSortedPhobias = [];
            for (String i in LineSplitter().convert(phobias)) {
              for (String t in _textEditingController.text.split('')) {
                if (i.split('-').first.toString().contains(t)) {
                  _listOfSortedPhobias.add(i);
                }
              }
            }
            _phobiasStream.add(_listOfSortedPhobias);
          });
    
      @override
      void initState() {
        super.initState();
        _phobiasStream = StreamController<List<String>>();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: TextField(
              controller: _textEditingController,
              onChanged: (text) {
                print("Text $text");
                _loadPhobias();
              },
            ),
          ),
          body: StreamBuilder(
            stream: _phobiasStream.stream,
            builder: (context, snapshot) {
              return snapshot.hasData
                  ? Container(
                      height: 300,
                      child: ListView.builder(
                        itemCount: snapshot.data.length,
                        itemBuilder: (context, index) {
                          print("Data ${snapshot.data[index]}");
                          return Text(snapshot.data[index]);
                        },
                      ),
                    )
                  : Center(
                      child: CircularProgressIndicator(),
                    );
            },
          ),
        );
      }
    }
    

    As seen in the code above, I eliminated unnecessary text change callbacks inside the for a loop.

    lib/phobia.txt

    test1-test2-test3-test4-test5
    

    Let me know if this is the expected scenario.

    Hope this helps.