Flutter嵌套的StreamBuilders导致Bad状态:Stream已被监听

时间:2018-04-10 23:04:37

标签: architecture dart flutter

我尝试使用视频Flutter / AngularDart – Code sharing, better together (DartConf 2018)中描述的BLoC模式构建Flutter应用

BLoC基本上是一个带有Sink输入和Stream输出的视图模型。在我的例子中,它看起来有点像这样:

 class BLoC {
    // inputs
    Sink<String> inputTextChanges;
    Sink<Null> submitButtonClicks;

    // outputs
    Stream<bool> showLoading;
    Stream<bool> submitEnabled;
 }

我在层次结构根目录附近的小部件中定义了BLoC,并将其传递给它下面的小部件,包括嵌套的StreamBuilders。像这样:

BLoC widget hierarchy

顶部StreamBuilder侦听BLoC上的showLoading流,以便它可以重建以显示重叠的进度微调器。底部StreamBuilder侦听submitEnabled流以启用/禁用按钮。

问题是,只要showLoading流导致顶层StreamBuilder重建窗口小部件,它就会重建嵌套窗口小部件。这本身就很好并且预期。但是,这会导致重新创建底部StreamBuilder。发生这种情况时,它会尝试重新订阅BLoC上的现有submitEnabled流,从而导致Bad state: Stream has already been listened to

有没有办法在不创建所有输出BroadcastStreams的情况下完成此任务?

(我也有可能从根本上误解BLoC模式。)

下面的实际代码示例:

import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'dart:async';

void main() => runApp(BlocExampleApp());

class BlocExampleApp extends StatefulWidget {

  BlocExampleApp({Key key}) : super(key: key);

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

class _BlocExampleAppState extends State<BlocExampleApp> {

  Bloc bloc = Bloc();

  @override
  Widget build(BuildContext context) =>
      MaterialApp(
        home: Scaffold(
            appBar: AppBar(elevation: 0.0),
            body: new StreamBuilder<bool>(
                stream: bloc.showLoading,
                builder: (context, snapshot) =>
                snapshot.data
                    ? _overlayLoadingWidget(_buildContent(context))
                    : _buildContent(context)
            )
        ),
      );

  Widget _buildContent(context) =>
      Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            TextField(
              onChanged: bloc.inputTextChanges.add,
            ),
            StreamBuilder<bool>(
                stream: bloc.submitEnabled,
                builder: ((context, snapshot) =>
                    MaterialButton(
                      onPressed: snapshot.data ? () => bloc.submitClicks.add(null) : null,
                      child: Text('Submit'),
                    )
                )
            )
          ]
      );

  Widget _overlayLoadingWidget(Widget content) =>
      Stack(
        children: <Widget>[
          content,
          Container(
            color: Colors.black54,
          ),
          Center(child: CircularProgressIndicator()),
        ],
      );
}

class Bloc {
  final StreamController<String> _inputTextChanges = StreamController<String>();
  final StreamController<Null> _submitClicks = StreamController();

  // Inputs
  Sink<String> get inputTextChanges => _inputTextChanges.sink;

  Sink<Null> get submitClicks => _submitClicks.sink;

  // Outputs
  Stream<bool> get submitEnabled =>
      Observable<String>(_inputTextChanges.stream)
          .distinct()
          .map(_isInputValid);

  Stream<bool> get showLoading => _submitClicks.stream.map((_) => true);

  bool _isInputValid(String input) => true;

  void dispose() {
    _inputTextChanges.close();
    _submitClicks.close();
  }
}

2 个答案:

答案 0 :(得分:3)

据我了解BLoC,您应该只有一个连接到StreamBuilder的输出流。此输出流发出包含所有必需状态的模型。

你可以在这里看到它是如何完成的: https://github.com/ReactiveX/rxdart/blob/master/example/flutter/github_search/lib/github_search_widget.dart

新链接: https://github.com/ReactiveX/rxdart/blob/master/example/flutter/github_search/lib/search_widget.dart

如果您需要组合多个流来生成模型(sowLoading和submitEnabled),则可以使用RxDart中的Observable.combineLatest将多个流合并为一个流。我使用这种方法,效果非常好。

答案 1 :(得分:0)

使用BehaviorSubject代替StreamController.BehaviorSubject将最近的事件发送给消费者