Flutter How to refresh StreamBuilder?
Declare a StreamController
with broadcast
, then set a friendly name to the Stream
of this StreamController
, then everytime you want to rebuild the wraped widget (the child of the StreamBuilder
just use the sink
property of the StreamController
to add
a new value that will trigger the StreamBuilder
.
You can use StreamBuilder
and AsyncSnapshot
without setting the type.
But if you use StreamBuilder<UserModel>
and AsyncSnapshot<UserModel>
when you type snapshot.data.
you will see all variables and methods from the UserModel
.
final StreamController<UserModel> _currentUserStreamCtrl = StreamController<UserModel>.broadcast();
Stream<UserModel> get onCurrentUserChanged => _currentUserStreamCtrl.stream;
void updateCurrentUserUI() => _currentUserStreamCtrl.sink.add(_currentUser);
StreamBuilder<UserModel>(
stream: onCurrentUserChanged,
builder: (BuildContext context, AsyncSnapshot<UserModel> snapshot) {
if (snapshot.data != null) {
print('build signed screen, logged as: ' + snapshot.data.displayName);
return blocs.pageView.pagesView; //pageView containing signed page
}
print('build login screen');
return LoginPage();
//print('loading');
//return Center(child: CircularProgressIndicator());
},
)
This way you can use a StatelessWidget
and refresh just a single sub-widget
(an icon with a different color, for example) without using setState
(that rebuilds the entire page).
For performance, streams are the best approach.
Edit:
I'm using BLoC architecture
approach, so it's much better to declare the variables in a homePageBloc.dart (that has a normal controller class with all business logic) and create the StreamBuilder in the homePage.dart (that has a class that extends Stateless/Stateful widget and is responsible for the UI).
Edit: My UserModel.dart
, you can use DocumentSnapshot
instead of Map<String, dynamic>
if you are using Cloud Firestore database from Firebase.
class UserModel {
/// Document ID of the user on database
String _firebaseId = "";
String get firebaseId => _firebaseId;
set firebaseId(newValue) => _firebaseId = newValue;
DateTime _creationDate = DateTime.now();
DateTime get creationDate => _creationDate;
DateTime _lastUpdate = DateTime.now();
DateTime get lastUpdate => _lastUpdate;
String _displayName = "";
String get displayName => _displayName;
set displayName(newValue) => _displayName = newValue;
String _username = "";
String get username => _username;
set username(newValue) => _username = newValue;
String _photoUrl = "";
String get photoUrl => _photoUrl;
set photoUrl(newValue) => _photoUrl = newValue;
String _phoneNumber = "";
String get phoneNumber => _phoneNumber;
set phoneNumber(newValue) => _phoneNumber = newValue;
String _email = "";
String get email => _email;
set email(newValue) => _email = newValue;
String _address = "";
String get address => _address;
set address(newValue) => _address = newValue;
bool _isAdmin = false;
bool get isAdmin => _isAdmin;
set isAdmin(newValue) => _isAdmin = newValue;
/// Used on first login
UserModel.fromFirstLogin() {
_creationDate = DateTime.now();
_lastUpdate = DateTime.now();
_username = "";
_address = "";
_isAdmin = false;
}
/// Used on any login that isn't the first
UserModel.fromDocument(Map<String, String> userDoc) {
_firebaseId = userDoc['firebaseId'] ?? '';
_displayName = userDoc['displayName'] ?? '';
_photoUrl = userDoc['photoUrl'] ?? '';
_phoneNumber = userDoc['phoneNumber'] ?? '';
_email = userDoc['email'] ?? '';
_address = userDoc['address'] ?? '';
_isAdmin = userDoc['isAdmin'] ?? false;
_username = userDoc['username'] ?? '';
//_lastUpdate = userDoc['lastUpdate'] != null ? userDoc['lastUpdate'].toDate() : DateTime.now();
//_creationDate = userDoc['creationDate'] != null ? userDoc['creationDate'].toDate() : DateTime.now();
}
void showOnConsole(String header) {
print('''
$header
currentUser.firebaseId: $_firebaseId
currentUser.username: $_username
currentUser.displayName: $_displayName
currentUser.phoneNumber: $_phoneNumber
currentUser.email: $_email
currentUser.address: $_address
currentUser.isAdmin: $_isAdmin
'''
);
}
String toReadableString() {
return
"displayName: $_displayName; "
"firebaseId: $_firebaseId; "
"email: $_email; "
"address: $_address; "
"photoUrl: $_photoUrl; "
"phoneNumber: $_phoneNumber; "
"isAdmin: $_isAdmin; ";
}
}
tonik
Updated on June 07, 2022Comments
-
tonik almost 2 years
Consider the following code:
StreamBuilder<QuerySnapshot> _createDataStream(){ return StreamBuilder<QuerySnapshot>( stream: Firestore.instance.collection("data").limit.(_myLimit).snapshots(), builder: (context, snapshot){ return Text(_myLimit.toString); } ); }
I want that the StreamBuilder refreshes when the
_myLimit
Variable changes. It's possible doing it like this:void _incrementLimit(){ setState(() =>_myLimit++); }
My Question is if there is another, faster way, except the
setState((){});
one. Because I don't want to recall the wholebuild()
method when the_myLimit
Variable changes.I figured out another Way but I feel like there is a even better solution because I think I don't make use of the
.periodic
functionality and I got a nested Stream I'm not sure how usual this is:Stream<int> myStream = Stream.periodic(Duration(), (_) => _myLimit); ... @override Widget build(BuildContext context){ ... return StreamBuilder<int>( stream: myStream, builder: (context, snapshot){ return _createDataStream; }, ), ... }
Solution(s)
import 'dart:async'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatefulWidget { @override State<StatefulWidget> createState() { return new _MyAppState(); } } class _MyAppState extends State<MyApp> { int myNum = 0; final StreamController _myStreamCtrl = StreamController.broadcast(); Stream get onVariableChanged => _myStreamCtrl.stream; void updateMyUI() => _myStreamCtrl.sink.add(myNum); @override void initState() { super.initState(); } @override void dispose() { _myStreamCtrl.close(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ StreamBuilder( stream: onVariableChanged, builder: (context, snapshot){ if(snapshot.connectionState == ConnectionState.waiting){ updateMyUI(); return Text(". . ."); } return Text(snapshot.data.toString()); }, ), RaisedButton( child: Text("Increment"), onPressed: (){ myNum++; updateMyUI(); }, ) ], ), ))); } }
Some other ideas, how the StreamBuilder also could look like:
StreamBuilder( stream: onVariableChanged, builder: (context, snapshot){ if(snapshot.connectionState == ConnectionState.waiting){ return Text(myNum.toString()); } return Text(snapshot.data.toString()); }, ),
StreamBuilder( stream: onVariableChanged, initialData: myNum, builder: (BuildContext context, AsyncSnapshot snapshot){ if(snapshot.data == null){ return Text("..."); } return Text(snapshot.data.toString()); }, ),
-
tonik about 4 yearsHey Gustavo, thanks for your Answer! I tried implementing it, but I'm doing something wrong, please look at the EDIT on my Question :)
-
Gustavo Contreiras about 4 yearsI think thats because the connection state waiting is the state of the stream till it closes on dispose. The stream will always be waiting for something to be added on sink. I dont use connection state. Compare if data is null, if is null probably is something being loaded, so render a progressa indicator, if its not you render the data or some other widget
-
tonik about 4 yearsThis does not fix it. Anyways, thank you for your help! I found another solution now, using the ValueListenableBuilder Widget.
-
Gustavo Contreiras about 4 yearsJust found your error: You don't need to call updateMyUI() on initState, and you HAVE to call every time you want to rebuild the StreamBuilder, in the RaisedButton on the onPressed you have to call updateMyUI() after increasing the number. Like this: onPressed: () { myNum++; updateMyUI(); } And use snapshot.data != null to show empty text
-
tonik about 4 yearsYes this works! But for some reason the Stream will still stuck in his return for data == null or ConnectionState.waiting, the Stream needs to be triggered. When I added the updateMyUI() in the data==null case or ConnectionState.waiting it works and the Stream is displaying the initial value of myNum. If you are checking for data == null there is also another way except calling updateMyUI() to trigger the Stream, you just could use the "initialData" property of the StreamBuilder and fill it with myNum. I will edit my question. Thank you!