Flutter: setState() does not trigger a build
Solution 1
I have the feeling, the answers don't address my question. My question was why the widget only builds once and why setState() does not trigger a second build.
Answers like "use a FutureBuilder" are not helpful since they completely bypass the question about setState()
. So no matter how late the async function finishes, triggering a rebuild should update the UI with the new list when setState()
is executed.
Also, the async function does not finish too early (before build has finished). I made sure it does not by trying WidgetsBinding.instance.addPostFrameCallback
which changed: nothing.
I figured out that the problem was somewhere else. In my main()
function the first two lines were:
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp,DeviceOrientation.portraitDown]
);
which somehow affected the build order. But only on my Huawei P20 Lite, on no other of my test devices, not in the emulator and not on Dartpad.
So conclusion:
Code is fine. My understanding of setState()
is also fine. I haven't provided enough context for you to reproduce the error. And my solution was to make the first two lines in the main()
function async:
void main() async {
await SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp,DeviceOrientation.portraitDown]
);
...
}
Solution 2
Your code works in DartPad.dev
Just copy/paste this to see it running:
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Scan(),
),
),
);
}
}
class Scan extends StatefulWidget {
@override
_ScanState createState() => _ScanState();
}
class _ScanState extends State<Scan> {
List<int> numbers;
@override
void initState() {
super.initState();
_initializeController();
}
@override
Widget build(BuildContext context) {
print('Build was scheduled');
return Center(
child: Text(
numbers == null ? '0' : numbers.length.toString()
)
);
}
Future<List<int>> _getAsyncNumberList() {
return Future(() => [1,2,3,4]);
}
_initializeController() async {
List<int> newNumbersList = await _getAsyncNumberList();
print("Number list was updated to list of length ${newNumbersList.length}");
setState(() {
numbers = newNumbersList;
});
}
}
Solution 3
I don't know why you say your code is not working, but here you can see that even the prints perform as they should. Your example might be oversimplified. If you add a delay to that Future (which is a real case scenario, cause fetching data and waiting for it does take a few seconds sometimes), then the code does indeed display 0.
The reason why your code works right now is that the Future returns the list instantly before the build method starts rendering Widgets. That's why the first thing that shows up on the screen is 4.
If you add that .delayed() to the Future, then it does indeed stop working, because the list of numbers is retrieved after some time and the build renders before the numbers are updated.
Problem explanation
SetState in your code is not called properly. You either do it like this (which in this case makes no sense because you use "await", but generally it works too)
_initializeController() async {
setState(() {
List<int> newNumbersList = await _getAsyncNumberList();
print("Number list was updated to list of length ${newNumbersList.length}");
numbers = newNumbersList;
});
}
or like this
_initializeController() async {
List<int> newNumbersList = await _getAsyncNumberList();
print("Number list was updated to list of length ${newNumbersList.length}");
numbers = newNumbersList;
setState(() {
/// this thing right here is an entire function. You MUST HAVE THE AWAIT in
/// the same function as the update, otherwise, the await is callledn, and on
/// another thread, the other functions are executed. In your case, this one
/// too. This one finishes early and updates nothing, and the await finishes later.
});
}
Suggested solution
This will display 0 while waiting 5 seconds for the Future to return the new list with the data and then it will display 4. If you want to display something else while waiting for the data, please use a FutureBuilder Widget.
FULL CODE WITHOUT FutureBuilder:
class Scan extends StatefulWidget {
@override
_ScanState createState() => _ScanState();
}
class _ScanState extends State<Scan> {
List<int> numbers;
@override
void initState() {
super.initState();
_initializeController();
}
@override
Widget build(BuildContext context) {
print('Build was scheduled');
return Center(
child: Text(numbers == null ? '0' : numbers.length.toString()));
}
Future<List<int>> _getAsyncNumberList() {
return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
}
_initializeController() async {
List<int> newNumbersList = await _getAsyncNumberList();
print(
"Number list was updated to list of length ${newNumbersList.length}");
numbers = newNumbersList;
setState(() {});
}
}
I strongly recommend using this version, since it displays something to the user the whole time while waiting for the data and also has a failsafe if an error comes up. Try them out and pick what is best for you, but again, I recommend this one.
FULL CODE WITH FutureBuilder:
class _ScanState extends State<Scan> {
List<int> numbers;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
print('Build was scheduled');
return FutureBuilder(
future: _getAsyncNumberList(),
builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return Center(child: Text('Fetching numbers...'));
default:
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
else
/// snapshot.data is the result that the async function returns
return Center(child: Text('Result: ${snapshot.data.length}'));
}
},
);
}
Future<List<int>> _getAsyncNumberList() {
return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
}
}
Here is a more detailed example with a full explanation of how FutureBuilder works. Take some time and carefully read through it. It's a very powerful thing Flutter offers.
Schnodderbalken
I am the liquid that comes out of your nose. My shape is a timber. In Germany we call that thing SCHNODDERBALKEN!
Updated on December 24, 2022Comments
-
Schnodderbalken over 1 year
I have a very simple (stateful) widget that contains a
Text
widget that displays the length of a list which is a member variable of the widget's state.Inside the
initState()
method, I override the list variable (formerly being null) with a list that has four elements usingsetState()
. However, theText
widget still shows "0".The prints I added imply that a rebuild of the widget has not been triggered although my perception was that this is the sole purpose of the
setState()
method.Here ist the code:
import 'package:flutter/material.dart'; class Scan extends StatefulWidget { @override _ScanState createState() => _ScanState(); } class _ScanState extends State<Scan> { List<int> numbers; @override void initState() { super.initState(); _initializeController(); } @override Widget build(BuildContext context) { print('Build was scheduled'); return Center( child: Text( numbers == null ? '0' : numbers.length.toString() ) ); } Future<List<int>> _getAsyncNumberList() { return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]); } _initializeController() async { List<int> newNumbersList = await _getAsyncNumberList(); print("Number list was updated to list of length ${newNumbersList.length}"); setState(() { numbers = newNumbersList; }); } }
My question: why does the widget only build once? I would have expected to have at least two builds, the second one being triggered by the execution of
setState()
. -
Admin over 3 yearsthis is not an answer, imo
-
Schnodderbalken over 3 yearsThank your for your answer. I understand what you're saying, but I still don't get why it's a problem to have the
_getAsyncNumberList()
completed afterbuild()
. CallingsetState()
's purpose is to trigger a build isn't it? So even if the execution of the function takes longer than the build, then callingsetState()
should update the widget and the text anyways, shouldn't it? -
Schnodderbalken over 3 yearsI edited the code using
Future.delayed
now. This makes it also happen in Dartpad not only on my phone. -
Mad World over 3 yearsI haven't said that is the problem. The problem is, your build renders before your method finished retrieving the numbers. Please don't just downvote people because you did not read the answer properly. Try out my code and you will see it works as it should. It displays something while waiting a few seconds for the result, and then the result is shown
-
Mad World over 3 yearsIn conclusion, in your initial code, where there was no delay, the numbers were retrieved before build, therefore the build displayed 4 on the screen straight up. SetState is not used properly in your code. Check my edit
-
Schnodderbalken over 3 yearsI wasn't me who downvoted your answer, sorry! Well, I thought the whole purpose of
await
is to wait for the execution of the function, turning an async call into a sync call. How cansetState()
be executed before the execution then? Apart from that: I can not put everything including theawait
ed function call insidesetState()
, even if I makesetState
async itself. See for yourself, the compiler will complain. If I do_getAsyncNumberList().then(() {setState(...)})
, it still does not work, which is from my understanding the same like my code now. -
Mad World over 3 yearsYeah, I forgot to mention the fact that the await makes no sense inside the setState. What was wrong with your code was just not placing all the connected parts outside the setState function. Just keep in mind it's best to leave setState empty and do your thing before and outside of it
-
Schnodderbalken over 3 yearsTechnically it doesn't matter whether to put the variable assignment inside the callback of setState or not. However, it's discouraged to have empty setState() callbacks. api.flutter.dev/flutter/widgets/ModalRoute/setState.html medium.com/@mehmetf_71205/setting-the-state-2809936fb79d So what's your source of information for "it's best to leave setState empty"? And why do you think it makes a difference?