Flutter事件在流中丢失

时间:2019-07-14 15:01:45

标签: flutter dart bloc

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

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

两个页面中的每个页面都有自己的BloC,但是由于需要将两个页面进行某种程度的连接才能从第一页到第二页获取所选项目,因此有一个第三AppBloC注入了其他两个BloC。它公开了一个接收器和一个流,以便在其他两个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();
  }

}

预期的行为是,每当我单击一个列表项时,第二页就会打开并显示该项目作为其标题。但这不是这里发生的事情。 执行上面的代码看起来像this。第一次单击列表项时,一切都按预期工作,并且字符串“ this”被设置为第二页的标题。但是关闭页面并再次执行此操作,将显示“ Nothing here:/”(StreamBuilder的默认字符串/初始值)。但是,正如您在屏幕截图中看到的那样,第三次该应用因出现异常而开始挂起:

Unhandled Exception: Bad state: Cannot add event after closing

当试图将接收到的字符串添加到接收器中时,第二页的BloC中会发生异常,以便可以将其显示为AppBar的标题:

  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已经关闭,因此发生了异常。因此,每次在以下情况下显示默认字符串时,基本上只是因为上一页仍然存在并捕获了该字符串...

所以我似乎无法弄清的部分是,第二件事发生在哪里?我是否错过了一些琐碎的事情或某处出现了问题?我在稳定通道(v1.7.8)和主通道(v1.8.2-pre.59)的多个不同android版本上对此进行了测试。我用的是dart 2.4.0。

1 个答案:

答案 0 :(得分:0)

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

enter code here

您的流监听器可以是公正流,而不是广播流

final _selectionController = BehaviorSubject<String>();

我之所以建议这样做,是因为RxDart的BehaviorSubject确保了无论何时收听,它总是在每个时间点都发出最后一个流。