web-dev-qa-db-fra.com

Flutter imbriqué StreamBuilders provoquant mauvais état: le flux a déjà été écouté

J'essaie de créer une application Flutter en utilisant le modèle BLoC décrit dans la vidéo Flutter/AngularDart - Partage de code, mieux ensemble (DartConf 2018)

Un BLoC est essentiellement un modèle de vue avec Sink entrées et Stream sorties. Dans mon exemple, cela ressemble un peu à ceci:

 class BLoC {
    // inputs
    Sink<String> inputTextChanges;
    Sink<Null> submitButtonClicks;

    // outputs
    Stream<bool> showLoading;
    Stream<bool> submitEnabled;
 }

Le BLoC est défini dans un widget situé près de la racine de la hiérarchie et transmis aux widgets situés en dessous, y compris la variable StreamBuilders imbriquée. Ainsi:

 BLoC widget hierarchy

La StreamBuilder supérieure écoute un flux showLoading sur le BLoC afin qu'il puisse se reconstruire pour afficher une spinner de progression superposée. La StreamBuilder inférieure écoute un flux submitEnabled pour activer/désactiver un bouton.

Le problème est que chaque fois que le flux showLoading force le StreamBuilder supérieur à reconstruire le widget, il reconstruit également les widgets imbriqués. C'est en soi bien et prévu. Cependant, la StreamBuilder inférieure est recréée. Lorsque cela se produit, il tente de se réabonner au flux submitEnabled existant sur le BLC, provoquant Bad state: Stream has already been listened to.

Y a-t-il un moyen d'accomplir cela sans rendre toutes les sorties BroadcastStreams?

(Il est également possible que je comprenne fondamentalement le motif BLoC.)


Exemple de code réel ci-dessous:

import 'package:flutter/material.Dart';
import 'package:rxdart/rxdart.Dart';
import 'Dart:async';

void main() => runApp(BlocExampleApp());

class BlocExampleApp extends StatefulWidget {

  BlocExampleApp({Key key}) : super(key: key);

  @override
  _BlocExampleAppState createState() => _BlocExampleAppState();
}

class _BlocExampleAppState extends State<BlocExampleApp> {

  Bloc bloc = Bloc();

  @override
  Widget build(BuildContext context) =>
      MaterialApp(
        home: Scaffold(
            appBar: AppBar(elevation: 0.0),
            body: new StreamBuilder<bool>(
                stream: bloc.showLoading,
                builder: (context, snapshot) =>
                snapshot.data
                    ? _overlayLoadingWidget(_buildContent(context))
                    : _buildContent(context)
            )
        ),
      );

  Widget _buildContent(context) =>
      Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            TextField(
              onChanged: bloc.inputTextChanges.add,
            ),
            StreamBuilder<bool>(
                stream: bloc.submitEnabled,
                builder: ((context, snapshot) =>
                    MaterialButton(
                      onPressed: snapshot.data ? () => bloc.submitClicks.add(null) : null,
                      child: Text('Submit'),
                    )
                )
            )
          ]
      );

  Widget _overlayLoadingWidget(Widget content) =>
      Stack(
        children: <Widget>[
          content,
          Container(
            color: Colors.black54,
          ),
          Center(child: CircularProgressIndicator()),
        ],
      );
}

class Bloc {
  final StreamController<String> _inputTextChanges = StreamController<String>();
  final StreamController<Null> _submitClicks = StreamController();

  // Inputs
  Sink<String> get inputTextChanges => _inputTextChanges.sink;

  Sink<Null> get submitClicks => _submitClicks.sink;

  // Outputs
  Stream<bool> get submitEnabled =>
      Observable<String>(_inputTextChanges.stream)
          .distinct()
          .map(_isInputValid);

  Stream<bool> get showLoading => _submitClicks.stream.map((_) => true);

  bool _isInputValid(String input) => true;

  void dispose() {
    _inputTextChanges.close();
    _submitClicks.close();
  }
}
8
Joeleski

Si je comprends bien BLoC, vous ne devriez avoir qu’un seul flux de sortie connecté à StreamBuilder. Ce flux de sortie émet un modèle qui contient tous les états requis. 

Vous pouvez voir comment cela se fait ici: https://github.com/ReactiveX/rxdart/blob/master/example/flutter/github_search/lib/github_search_widget.Dart

Si vous devez combiner plusieurs processus pour générer votre modèle (sowLoading et submitEnabled), vous pouvez utiliser Observable.combineLatest de RxDart pour fusionner plusieurs flux en un seul flux. J'utilise cette approche et ça marche vraiment bien.

3
BauerMitFackel

utilisez BehaviorSubject à la place, StreamController.BehaviorSubject enverra l'événement le plus proche au consommateur.

0
余艳辉