Недавно я начал использовать управление состоянием во флаттере и в значительной степени остановился на BloC. Однако я не использую блочный пакет или любую подобную зависимость для него, поскольку моя кодовая база не такая сложная, и мне нравится писать ее самостоятельно. Но я столкнулся с проблемой, которую никак не могу решить. Таким образом, у меня есть поток, который, кажется, просто теряет определенное событие каждый раз, когда я помещаю его в раковину.
Я создал пример приложения, которое намного проще, чем моя фактическая кодовая база, но все еще имеет эту проблему. Приложение состоит из двух страниц, на первой (главной) странице отображается список строк. Когда вы нажимаете на один из элементов списка, открывается вторая страница, и строка/элемент, на который вы нажали, будет отображаться на этой странице.
Каждая из двух страниц имеет собственный блок, но поскольку две страницы должны быть каким-то образом связаны, чтобы передать выбранный элемент с первой на вторую страницу, существует третий блок приложения, который внедряется в два других блока. Он предоставляет приемник и поток для отправки данных между двумя другими блоками.
Единственный сторонний пакет, используемый в этом примере, — это киви (0.2.0) для внедрения зависимостей.
мой основной дротик довольно прост и выглядит так:
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(),
},
);
}
}
то есть две страницы:
первый.дротик:
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,
);
}
}
второй.дротик:
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);
},
),
),
);
}
}
и, наконец, вот мои три блока:
блок.дротик:
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();
}
}
Ожидаемое поведение будет заключаться в том, что каждый раз, когда я нажимаю на один из элементов списка, вторая страница будет открываться и отображать этот элемент в качестве заголовка. Но это не то, что здесь происходит. Выполнение приведенного выше кода будет выглядеть как это. В первый раз, когда вы нажимаете на элемент списка, все работает так, как задумано, и строка «это» устанавливается в качестве заголовка второй страницы. Но закрывая страницу и повторяя это снова, отображается «Здесь ничего :/» (строка по умолчанию/начальное значение StreamBuilder). Однако в третий раз, как вы можете видеть на скриншоте, приложение начинает зависать из-за исключения:
Unhandled Exception: Bad state: Cannot add event after closing
Исключение возникает в блоке второй страницы при попытке добавить полученную строку в приемник, чтобы ее можно было отобразить как заголовок 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();
}
Теперь, когда больше нет исключений, которые остановят выполнение приложения, происходит следующее: первый раз такой же, как и раньше (отображается заголовок — ожидаемое поведение), но все последующие разы отображается строка по умолчанию, независимо от того, как часто вы пытаетесь это сделать. Теперь вы могли заметить две отладочные отпечатки в блок.дротик. Первый сообщает мне, когда событие добавляется в приемник 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...
Так что как вы можете ясно видеть, во второй раз событие как-то куда-то затерялось. Это также объясняет исключение, которое я получал раньше. Поскольку заголовок так и не попал на вторую страницу со второй попытки, Блок все еще ждал, пока событие пройдет через поток. Поэтому, когда я щелкнул элемент в третий раз, предыдущий блок все еще был активен и получил событие. Конечно, тогда страница и StreamController уже были закрыты, следовательно, исключение. Таким образом, каждый раз, когда отображается строка по умолчанию, следующие разы в основном просто потому, что предыдущая страница была еще жива и поймала строку...
Итак, часть, которую я не могу понять, это то, куда пошло это второе событие? Я пропустил что-то действительно тривиальное или где-то ошибся? Я тестировал это на стабильном канале (v1.7.8), а также на основном канале (v1.8.2-pre.59) на нескольких разных версиях Android. Я использовал дартс 2.4.0.





Вы можете попробовать использовать Rxdart BehaviorSubject вместо StreamController в своем основном AppBloc.
final _selectionController = BehaviorSubject<String>();
И ваш слушатель потока может быть просто потоком, а не широковещательным потоком.
selected = _selectionController.stream;
Причина, по которой я предлагаю это, заключается в том, что BehaviorSubject RxDart гарантирует, что он всегда испускает последний поток в каждый момент времени, где бы он ни прослушивался.
У меня ушло несколько часов, но теперь я понимаю, что вы имели в виду. Использование rxDart мне не помогло, как и использование обычного потока. Однако я не использовал их вместе. Теперь работает, спасибо!
На самом деле я пытался использовать BehaviorSubject rxDart, но, к сожалению, это ничего не изменило, потому что (я думаю) rxDart основан на Stream API Dart. Кроме того, мне нужен широковещательный поток, потому что, если я открываю и закрываю вторую страницу несколько раз, мне также нужно несколько раз подписываться на этот поток...