Flutter 事件在流中丢失

Flutter event gets lost in stream

我最近开始在 flutter 中使用状态管理,并且几乎决定使用 BloC。但是,我不使用 bloc package 或任何类似的依赖项,因为我的代码库并不那么复杂,而且我喜欢自己编写。但是我遇到了一个我似乎无法解决的问题。总而言之,我有一个流,每次我将它放入接收器时似乎都会丢失某个事件。

我构建了一个示例应用程序,它比我的实际代码库简单得多,但仍然存在这个问题。该应用程序由两个页面组成,第一个(主)页面显示字符串列表。当您单击其中一个 list-items 时,第二个页面将打开,您单击的 string/the 项目将显示在此页面上。

这两个页面中的每一个都有自己的 BloC,但是由于这两个页面需要某种程度的连接才能从第一页到第二页获取所选项目,所以有第三个 AppBloC 被注入到另外两个页面中集团。它公开了一个接收器和一个流以在其他两个 BloC 之间发送数据。

此示例中使用的唯一第三方包是 kiwi (0.2.0) 用于依赖项注入。

我的 main.dart 非常简单,看起来像这样:

import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart' as kw; //renamed to reduce confusion with flutter's own Container widget
import 'package:streams_bloc_test/first.dart';
import 'package:streams_bloc_test/second.dart';
import 'bloc.dart';


kw.Container get container => kw.Container(); //Container is a singleton used for dependency injection with Kiwi

void main() {
  container.registerSingleton((c) => AppBloc()); //registering AppBloc as a singleton for dependency injection (will be injected into the other two blocs)
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final appBloc = container.resolve(); //injecting AppBloc here just to dispose it when the App gets closed

  @override
  void dispose() {
    appBloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp( //basic MaterialApp with two routes
      title: 'Streams Test',
      theme: ThemeData.dark(),
      initialRoute: "first",
      routes: {
        "first": (context) => FirstPage(),
        "first/second": (context) => SecondPage(),
      },
    );
  }
}

然后是两个页面:
first.dart:

import 'package:flutter/material.dart';
import 'package:streams_bloc_test/bloc.dart';

class FirstPage extends StatefulWidget { //First page that just displays a simple list of strings
  @override
  _FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  final bloc = FirstBloc();

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("FirstPage")),
      body: StreamBuilder<List<String>>(
          initialData: [],
          stream: bloc.list,
          builder: (context, snapshot) {
            return ListView.builder( //displays list of strings from the stream
              itemBuilder: (context, i){
                return ListItem(
                  text: snapshot.data[i],
                  onTap: () { //list item got clicked
                    bloc.selectionClicked(i); //send selected item to second page
                    Navigator.pushNamed(context, "first/second"); //open up second page
                  },
                );
              },
              itemCount: snapshot.data.length,
            );
          }),
    );
  }
}

class ListItem extends StatelessWidget { //simple widget to display a string in the list
  final void Function() onTap;
  final String text;

  const ListItem({Key key, this.onTap, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return InkWell(
      child: Container(
        padding: EdgeInsets.all(16.0),
        child: Text(text),
      ),
      onTap: onTap,
    );
  }
}

second.dart:

import 'package:flutter/material.dart';
import 'package:streams_bloc_test/bloc.dart';

class SecondPage extends StatefulWidget { //Second page that displays a selected item
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  final bloc = SecondBloc();

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: StreamBuilder( //selected item is displayed as the AppBars title
          stream: bloc.title,
          initialData: "Nothing here :/", //displayed when the stream does not emit any event
          builder: (context, snapshot) {
            return Text(snapshot.data);
          },
        ),
      ),
    );
  }
}

最后是我的三个 BloC:
bloc.dart:

import 'dart:async';
import 'package:kiwi/kiwi.dart' as kw;

abstract class Bloc{
  void dispose();
}

class AppBloc extends Bloc{ //AppBloc for connecting the other two Blocs
  final _selectionController = StreamController<String>(); //"connection" used for passing selected list items from first to second page

  Stream<String> selected;
  Sink<String> get select => _selectionController.sink;

  AppBloc(){
    selected = _selectionController.stream.asBroadcastStream(); //Broadcast stream needed if second page is opened/closed multiple times
  }

  @override
  void dispose() {
    _selectionController.close();
  }
}

class FirstBloc extends Bloc { //Bloc for first Page (used for displaying a simple list)
  final appBloc = kw.Container().resolve<AppBloc>(); //injected AppBloc
  final listItems = ["this", "is", "a", "list"]; //example list items

  final _listController = StreamController<List<String>>();

  Stream<List<String>> get list => _listController.stream;

  FirstBloc(){
    _listController.add(listItems); //initially adding list items
  }

  selectionClicked(int index){ //called when a list item got clicked
    final item = listItems[index]; //obtaining item
    appBloc.select.add(item); //adding the item to the "connection" in AppBloc
    print("item added: $item"); //debug print
  }

  @override
  dispose(){
    _listController.close();
  }
}

class SecondBloc extends Bloc { //Bloc for second Page (used for displaying a single list item)
  final appBloc = kw.Container().resolve<AppBloc>(); //injected AppBloc

  final _titleController = StreamController<String>(); //selected item is displayed as the AppBar title

  Stream<String> get title => _titleController.stream;

  SecondBloc(){
    awaitTitle(); //needs separate method because there are no async constructors
  }

  awaitTitle() async {
    final title = await appBloc.selected.first; //wait until the "connection" spits out the selected item
    print("recieved title: $title"); //debug print
    _titleController.add(title); //adding the item as the title
  }

  @override
  void dispose() {
    _titleController.close();
  }

}

预期的行为是,每次我单击 list-items 之一时,第二页都会打开并显示该项目作为其标题。但这不是这里发生的事情。 执行上面的代码看起来像 this。第一次单击列表项时,一切正常,字符串 "this" 被设置为第二页的标题。但是关闭页面并再次这样做,会显示 "Nothing here :/"(StreamBuilder 的默认 string/initial 值)。然而,第三次,正如您在屏幕截图中看到的那样,应用程序由于异常而开始挂起:

Unhandled Exception: Bad state: Cannot add event after closing

当尝试将接收到的字符串添加到接收器中以显示为 AppBar 的标题时,第二页的 BloC 中出现异常:

  awaitTitle() async {
    final title = await appBloc.selected.first;
    print("recieved title: $title");
    _titleController.add(title); //<-- thats where the exception get's thrown
  } 

起初这似乎有点奇怪。 StreamController (_titleController) 只有在页面也关闭时才会关闭(而且页面显然还没有关闭)。那么为什么会抛出这个异常呢? 因此,为了好玩,我取消了 _titleController 关闭的行的注释。它可能会造成一些内存泄漏,但这对于调试来说很好:

  @override
  void dispose() {
    //_titleController.close();
  }

既然没有更多的异常会阻止应用程序执行,则会发生以下情况:第一次与以前相同(显示标题 - 预期行为),但接下来的所有时间都会获取默认字符串显示,无论您尝试多少次。现在您可能已经注意到 bloc.dart 中的两个调试打印。第一个告诉我何时将事件添加到 AppBloc 的接收器,第二个告诉我何时收到事件。这是输出:

//first time
  item added: this
  recieved title: this
//second time
  item added: this
//third time
  item added: this
  recieved title: this
//all the following times are equal to the third time...

所以你可以清楚地看到,第二次事件不知何故在某个地方丢失了。这也解释了我之前遇到的异常。由于标题在第二次尝试时从未进入第二页,因此 BloC 仍在等待事件通过流。因此,当我第三次单击该项目时,前一个集团仍处于活动状态并收到了该事件。当然,页面和 StreamController 已经关闭,因此例外。所以每次默认字符串显示如下次数基本上只是因为前一个页面还活着并捕获了字符串...

所以我似乎无法弄清楚的部分是,第二个事件去哪儿了?我错过了一些非常微不足道的事情还是在某处出错了?我在多个不同 android 版本的稳定频道 (v1.7.8) 和主频道 (v1.8.2-pre.59) 上对此进行了测试。我用的是 dart 2.4.0.

您可以尝试在主 AppBloc 中使用 Rxdart 的 BehaviorSubject 而不是 StreamController

final _selectionController = BehaviorSubject<String>();

并且您的流监听器可以是纯流而不是广播流

selected = _selectionController.stream;

我之所以这么建议是因为 RxDart 的 BehaviorSubject 确保它总是在每个时间点发出最后一个流,无论它在哪里被收听。