I have an async stream which generates consecutive integers every second:
Stream<int> f() async* {
for (int i = 0; i < 100; ++i) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
I have a state List<Widget> l = [];
and want to append a Card
to it every second by using the stream.
@override
void initState() {
super.initState();
f().listen((d) {
setState(
() => this.l.add(Card(child: ListTile(title: Text(d.toString())))));
});
}
And here’s the build()
method:
@override
Widget build(BuildContext context) {
print('build(): ${this.l.length}');
return ListView(children: this.l);
}
When I run the program via flutter run -d macos
(i.e. macOS desktop app), display isn’t updated at all although build()
is called and this.l
is updated every second. The console output is
flutter: build(): 0
flutter: build(): 1
flutter: build(): 2
flutter: build(): 3
flutter: build(): 4
flutter: build(): 5
...
If I press r key to perform hot reload, display is updated.
Why?
How to reproduce:
-
flutter run -d macos
to start the application. -
Even if
build()
is called every second, display remains fully black. -
If you press r key to perform hot reload, display is updated.
Full code here:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.blueGrey,
useMaterial3: true),
home: Scaffold(body: W()),
debugShowCheckedModeBanner: false,
);
}
}
class W extends StatefulWidget {
const W({
super.key,
});
@override
State<W> createState() => _WState();
}
class _WState extends State<W> {
List<Widget> l = [];
@override
void initState() {
super.initState();
Stream<int> f() async* {
for (int i = 0; i < 100; ++i) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
f().listen((d) {
setState(
() => this.l.add(Card(child: ListTile(title: Text(d.toString())))));
});
}
@override
Widget build(BuildContext context) {
print('build(): ${this.l.length}');
return ListView(children: this.l);
}
}
3
Answers
Try to change
ListView()
intoListView.builder(itemCount: l.length, itemBuilder: (_, i) => l[i])
.It’s because
ListView
widget.ListView
likes a static widget, it receives only the beginning argument listl
and does not update even you call setState.More Strict Explanation
The official document of
ListView()
constructor saysand the embedded link says
Use builder instead
You shouldn’t cache widgets. The preferred approach is to store the state – in this case the list of strings received. In build, you then create the widgets on the fly.