7

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().)

Der Programmer
  • 77
  • 1
  • 1
  • 6
  • Hi Der, would you be able to share what's inside the `textEditingController` text change listener? – Joshua de Guzman Feb 08 '20 at 13:26
  • @juliano `for (String i in LineSplitter().convert(phobias)) { _textEditingController.addListener(() { for (String t in _textEditingController.text.split('')) { if (i.split('-').first.toString().contains(t)) { _listOfSortedPhobias.add(i); } } }); } return _listOfSortedPhobias;` – Der Programmer Feb 08 '20 at 14:54
  • @juliano the texteditingcontroller returns datas that has no problem to be used, the issue is about the futurebuilder update – Der Programmer Feb 08 '20 at 14:57

2 Answers2

13

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.

Joshua de Guzman
  • 2,063
  • 9
  • 24
2

The solution can be inferred in the third line of the error message:

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().)

So this means you'll have to perform the operation before refreshing the widget. You can have a temporary variable to hold the result of the asynchronous work and use that in your setState method:

onChanged: (text) {
     setState(() => _futureData =  _loadPhobias())
},

Could be written as:

onChanged: (text) async {
    var phobias = _loadPhobias();
    setState(() {
        _futureData = phobias;
    });
},
Victor Eronmosele
  • 7,040
  • 2
  • 10
  • 33
  • the _futureData is a Future variable, it couldn't be assigned to the phobias which is a List. Do you have any idea to allow my futurebuilder to refresh? I read other posts but it don't work for me – Der Programmer Feb 08 '20 at 14:54
  • What if you removed the await keyword before _loadPhobias() ? Like in the updated answer – Victor Eronmosele Feb 09 '20 at 07:48
  • but then you lose the ability to show a loading indicator or error without coding something around it just to circumvent this issue. :( – johnw182 Mar 14 '22 at 23:56