【问题标题】:Flutter event gets lost in streamFlutter 事件在流中丢失
【发布时间】:2023-03-09 04:05:01
【问题描述】:

我最近开始在 Flutter 中使用状态管理,并且非常喜欢 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 已经关闭,因此异常。所以每次显示默认字符串后面的次数基本上只是因为上一页还活着,抓到了字符串...

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

【问题讨论】:

    标签: flutter dart bloc


    【解决方案1】:

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

    final _selectionController = BehaviorSubject<String>();
    

    你的流监听器可以是一个公正的流而不是一个广播流

    selected = _selectionController.stream;
    

    我建议这样做的原因是因为 RxDart 的 BehaviorSubject 确保它始终在每个时间点发出最后一个流,无论它被收听。

    【讨论】:

    • 我实际上已经尝试过使用 rxDart 的 BehaviorSubject 但不幸的是这并没有改变任何东西,因为(我认为)rxDart 是基于 Dart 的 Stream API。另外,我确实需要一个广播流,因为如果我多次打开和关闭第二页,我还需要多次订阅该流...
    • 花了我几个小时,但现在我明白你的意思了。使用 rxDart 对我没有帮助,使用常规流也没有帮助。但是我没有将两者结合使用。它现在可以工作了,谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-17
    相关资源
    最近更新 更多