Skip to content

Nested async generator doesn't work as expected #91096

@omatt

Description

@omatt

Steps to Reproduce

Minimal repro

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(
      title: 'Unexpected Behavior with Nested Async Generators in Dart',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CounterWidget(),
    );
  }
}

class CounterWidget extends StatefulWidget {
  CounterWidget({Key? key}) : super(key: key);

  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  var correct = <int>[];
  var wrong = <int>[];

  final blocCorrect = Counter();
  final blocWrong = Counter();

  @override
  void initState() {
    super.initState();
    blocCorrect.listen((i) {
      correct.add(i);
      setState(() {});
    });

    blocWrong.listen((i) {
      wrong.add(i);
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              correct.toString(),
              style: TextStyle(color: Colors.black),
            ),
            SizedBox(height: 50),
            TextButton(
              child: Text('Correct'),
              onPressed: () => blocCorrect.add(CounterEvent.correct),
            ),
            SizedBox(height: 50),
            Container(
              color: Colors.black,
              width: 200,
              height: 2,
            ),
            SizedBox(height: 50),
            Text(
              wrong.toString(),
              style: TextStyle(color: Colors.black),
            ),
            SizedBox(height: 50),
            TextButton(
              child: Text('Wrong'),
              onPressed: () => blocWrong.add(CounterEvent.wrong),
            ),
          ],
        ),
      ),
    );
  }
}

enum CounterEvent { correct, wrong }

class Counter extends Bloc<CounterEvent, int> {
  Counter() : super(0);

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    if (event == CounterEvent.correct) {
      // Works
      yield state + 1;
      yield state + 1;
    } else {
      /// Does not work
      yield* _increment();
    }
  }

  Stream<int> _increment() async* {
    yield state + 1;
    yield state + 1;
  }
}

abstract class Bloc<E, S> {
  Bloc(this._state) {
    _eventController.stream.asyncExpand(mapEventToState).listen((s) {
      _state = s;
      _stateController.add(_state);
    });
  }

  final _eventController = StreamController<E>();
  final _stateController = StreamController<S>.broadcast();

  S _state;
  S get state => _state;

  StreamSubscription<S> listen(void Function(S) onData) {
    return _stateController.stream.listen(onData);
  }

  Stream<S> mapEventToState(E event);

  void add(E event) => _eventController.add(event);
}
  1. Run minimal repro
  2. Click 'Correct', prints 1,2
  3. Click 'Wrong', prints 1,1

Expected results:

Clicking 'Wrong' should print 1,2 - similar to the output of clicking 'Correct'

Actual results:

yield state doesn't get update as expected. Clicking 'Wrong' prints 1,1

Demo

As a workaround, _increment() can be made recursive. Calling yield* _increment(2) prints 1,2

Stream<int> _increment(int n) async* {
  if (n > 0) {
    yield state + 1;
    yield* _increment(n - 1);
  }
}
Logs
[✓] Flutter (Channel stable, 2.5.1, on macOS 11.6 20G165 darwin-x64, locale en)
    • Flutter version 2.5.1
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ffb2ecea52 (2 weeks ago), 2021-09-17 15:26:33 -0400
    • Engine revision b3af521a05
    • Dart version 2.14.2

[!] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    • Android SDK
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.

[✓] Xcode - develop for iOS and macOS
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 12.0.1, Build version 12A7300
    • CocoaPods version 1.10.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2020.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7281165)

[✓] VS Code (version 1.60.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.26.0

[✓] Connected device (1 available)
    • Chrome (web) • chrome • web-javascript • Google Chrome 94.0.4606.61

! Doctor found issues in 1 category.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions