Flutter infinite loop when StreamBuilder inside LayoutBuilder
This happens because of a misuse of streams.
The culprit is this line:
Observable<String> get emailStream => _emailController.stream.transform(...);
The issue with this line is that it creates a new stream every time.
This means that bloc.emailStream == bloc.emailStream
is actually false.
When combined with StreamBuilder
, it means that every time something asks StreamBuilder
to rebuild, the latter will restart the listening process from scratch.
Instead of a getter, you should create the stream once inside the constructor body of your BLoC:
class MyBloc {
StreamController _someController;
Stream foo;
MyBloc() {
foo = _someController.stream.transform(...);
}
}
Manos Serifios
Updated on December 11, 2022Comments
-
Manos Serifios over 1 year
So i am making a page with a LayoutBuilder as described here
Inside the LayoutBuilder i put a StreamBuilder with a TextField powered by the bloc class SignupFormBloc. The stream is a BehaviorSubject
When someone put something in the input it trigger the onChanged function which is the sink for my stream. So i add the value in the stream then i pass the value in a StreamTransformer to validate the value and then i let the StreamBuilder to build the TextField again with an error message(if value not valid).
This is were the problem starts.
When i click on the TextField and enter something it starts an infinite loop like this:
- The StreamBuilder sees the new value in the stream
- The StreamBuilder try to rebuild TextField
- Some how this triggers the LayoutBuilder builder function
- The LayoutBuilder builder function builds again the StreamBuilder
- StreamBuilder find a value in stream(because of the BehaviorSubject)
- and all start again from the first bulled in an endless loop
Hint: If i change the BehaviorSubject to a PublishSubject everything is ok
Hint 2: If i remove the StreamBuilder completely and just let a blank TextField, you can see that in every entry the LayoutBuilder builder function run. Is that a normal behavior?
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { SignupFormBloc _signupFormBloc; @override void initState() { super.initState(); _signupFormBloc = SignupFormBloc(); } @override Widget build(BuildContext context) { print('Build Run!!!!!'); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: LayoutBuilder( builder: (BuildContext context, BoxConstraints viewportConstraints) { print('Layout Builder!!!'); return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, ), child: IntrinsicHeight( child: StreamBuilder<String>( stream: _signupFormBloc.emailStream, builder: (context, AsyncSnapshot<String> snapshot) { return TextField( onChanged: _signupFormBloc.onEmailChange, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( hintText: 'Email', contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18), filled: true, fillColor: Colors.white, errorText: snapshot.error, border: new OutlineInputBorder( borderSide: BorderSide.none ), ), ); } ), ), ), ); }, ) ); } @override void dispose() { _signupFormBloc?.dispose(); super.dispose(); } } class SignupFormBloc { /// /// StreamControllers /// BehaviorSubject<String> _emailController = BehaviorSubject<String>(); /// /// Stream with Validators /// Observable<String> get emailStream => _emailController.stream.transform(StreamTransformer<String,String>.fromHandlers(handleData: (email, sink){ final RegExp emailExp = new RegExp(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"); if (!emailExp.hasMatch(email) || email.isEmpty){ print('has error'); sink.addError('Email format is invalid'); } else { sink.add(email); } })); /// /// Sinks /// Function(String) get onEmailChange => _emailController.sink.add; void dispose() { _emailController.close(); } }