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:
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();
}
}
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.
utilisez BehaviorSubject à la place, StreamController.BehaviorSubject enverra l'événement le plus proche au consommateur.