Событие Flutter теряется в потоке

Недавно я начал использовать управление состоянием во флаттере и в значительной степени остановился на 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.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
4 095
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете попробовать использовать Rxdart BehaviorSubject вместо StreamController в своем основном AppBloc.

final _selectionController = BehaviorSubject<String>();

И ваш слушатель потока может быть просто потоком, а не широковещательным потоком.

selected = _selectionController.stream;

Причина, по которой я предлагаю это, заключается в том, что BehaviorSubject RxDart гарантирует, что он всегда испускает последний поток в каждый момент времени, где бы он ни прослушивался.

На самом деле я пытался использовать BehaviorSubject rxDart, но, к сожалению, это ничего не изменило, потому что (я думаю) rxDart основан на Stream API Dart. Кроме того, мне нужен широковещательный поток, потому что, если я открываю и закрываю вторую страницу несколько раз, мне также нужно несколько раз подписываться на этот поток...

Leon P 14.07.2019 18:20

У меня ушло несколько часов, но теперь я понимаю, что вы имели в виду. Использование rxDart мне не помогло, как и использование обычного потока. Однако я не использовал их вместе. Теперь работает, спасибо!

Leon P 14.07.2019 22:22

Другие вопросы по теме