【发布时间】: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。
【问题讨论】: