Search code examples
flutterdartstreamlisteneronchange

Flutter How to refresh StreamBuilder?


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 whole build() 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());
  },
),

Solution

  • 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; ";
      }
    }