skip to Main Content

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:

  1. flutter run -d macos to start the application.

  2. Even if build() is called every second, display remains fully black.

    enter image description here

  3. If you press r key to perform hot reload, display is updated.

    enter image description here

Full code here:

DartPad

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


  1. Try to change ListView() into ListView.builder(itemCount: l.length, itemBuilder: (_, i) => l[i]).

    It’s because ListView widget. ListView likes a static widget, it receives only the beginning argument list l and does not update even you call setState.

    More Strict Explanation

    The official document of ListView() constructor says

    Like other widgets in the framework, this widget expects that the children list will not be mutated after it has been passed in here. See the documentation at SliverChildListDelegate.children for more details.

    and the embedded link says

    Also, a Widget in Flutter is immutable, so directly modifying the children such as someWidget.children.add(...) or passing a reference of the original list value to the children parameter will result in incorrect behaviors. Whenever the children list is modified, a new list object should be provided.

    Login or Signup to reply.
  2. Use builder instead

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key key}) : super(key: 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({Key key}) : super(key: key);
    
      @override
      State<W> createState() => _WState();
    }
    
    class _WState extends State<W> {
      StreamController<int> streamController;
      List<String> list = []; // change the string to any data object
    
      @override
      void initState() {
        super.initState();
    
        streamController = StreamController<int>();
    
        Stream<int> f() async* {
          for (int i = 0; i < 100; ++i) {
            await Future.delayed(Duration(seconds: 1));
            yield i;
          }
        }
    
        f().listen((d) {
          setState(() {
            list.add(d.toString());
          });
        });
      }
    
      @override
      void dispose() {
        streamController.close();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        print('build(): $itemCount');
        return ListView.builder(
          itemCount: itemCount,
          itemBuilder: (BuildContext context, int index) {
            final listItem = list[index];
            return Card(
              key: Key('card_$index'),
              child: ListTile(
                title: Text(listItem),
              ),
            );
          },
        );
      }
    }
    
    Login or Signup to reply.
  3. 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.

    class _WState extends State<W> {
      final strings = <String>[];
    
      @override
      void initState() {
        super.initState();
    
        Stream<int> f() async* {
          for (int i = 0; i < 100; ++i) {
            await Future.delayed(const Duration(seconds: 1));
            yield i;
          }
        }
    
        f().listen((d) {
          setState(() {
            strings.add(d.toString());
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return ListView(
            children: strings
                .map<Widget>((s) => ListTile(
                      title: Text(s),
                    ))
                .toList());
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search