Flutter: implementing a search feature for data from a StreamBuilder with ListView

6,077

You can take the following approach.

  1. You receive the complete data in snapshot.
  2. Have a hierarchy of widgets like : StreamBuilder( ValueListenableBuilder( ListView.Builder ) )
  3. Create ValueNotifier and give it to ValueListenable builder.
  4. Use Search view to change value of ValueNotifier.
  5. When value of ValueNotifier will change your ListView.builder will rebuild and at that time if you are giving the filtered list according to query to the ListView.builder then it will work for you the way you want it.

I hope this helps, in case of any doubt please let me know.

EDITED

You won't need ValueNotifier instead You will need a StreamBuilder. So your final hierarchy of widgets would be: StreamBuilder( StreamBuilder>( ListView.Builder ) )

I didn't have an environment like yours so I have mocked it and created an example. You can refer it and I hope it can give you some idea to solve your problem. Following is a working code which you can refer:

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: SearchWidget(),
      ),
    ));

class SearchWidget extends StatelessWidget {
  SearchWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          TextField(onChanged: _filter),
          StreamBuilder<List<User>>( // StreamBuilder<QuerySnapshot> in your code.
            initialData: _dataFromQuerySnapShot, // you won't need this. (dummy data).
            // stream: Your querysnapshot stream.
            builder:
                (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
              return StreamBuilder<List<User>>(
                key: ValueKey(snapshot.data),
                initialData: snapshot.data,
                stream: _stream,
                builder:
                    (BuildContext context, AsyncSnapshot<List<User>> snapshot) {
                      print(snapshot.data);
                  return ListView.builder(
                    shrinkWrap: true,
                    itemCount: snapshot.data.length,
                    itemBuilder: (BuildContext context, int index) {
                      return Text(snapshot.data[index].name);
                    },
                  );
                },
              );
            },
          )
        ],
      ),
    );
  }
}

StreamController<List<User>> _streamController = StreamController<List<User>>();
Stream<List<User>> get _stream => _streamController.stream;
_filter(String searchQuery) {
  List<User> _filteredList = _dataFromQuerySnapShot
      .where((User user) => user.name.toLowerCase().contains(searchQuery.toLowerCase()))
      .toList();
  _streamController.sink.add(_filteredList);
}

List<User> _dataFromQuerySnapShot = <User>[
  // every user has same enviornment because you are applying
  // such filter on your query snapshot.
  // same is the reason why every one is approved user.
  User('Zain Emery', 'some_enviornment', true),
  User('Dev Franco', 'some_enviornment', true),
  User('Emilia ONeill', 'some_enviornment', true),
  User('Zohaib Dale', 'some_enviornment', true),
  User('May Mcdougall', 'some_enviornment', true),
  User('LaylaRose Mitchell', 'some_enviornment', true),
  User('Beck Beasley', 'some_enviornment', true),
  User('Sadiyah Walker', 'some_enviornment', true),
  User('Mae Malone', 'some_enviornment', true),
  User('Judy Mccoy', 'some_enviornment', true),
];

class User {
  final String name;
  final String environment;
  final bool approved;

  const User(this.name, this.environment, this.approved);

  @override
  String toString() {
    return 'name: $name environment: $environment approved: $approved';
  }
}
Share:
6,077
Lorence Cramwinckel
Author by

Lorence Cramwinckel

Flutter beginner and overall coding enthusiast!

Updated on December 16, 2022

Comments

  • Lorence Cramwinckel
    Lorence Cramwinckel over 1 year

    In my Flutter app I have a screen with all the users. The list of users is generated by a StreamBuilder, which gets the data from Cloud Firestore and displays the users in a ListView. To improve functionality I want to be able to search through this user list with a search bar in the Appbar.

    I have tried this answer and that worked well but I can't figure out how to get it working with a StreamBuilder in my case. As a flutter beginner I would appreciate any help! Below I have included my user screen and the StreamBuilder.

    import 'package:flutter/material.dart';
    import 'package:cloud_firestore/cloud_firestore.dart';
    import 'package:firebase_auth/firebase_auth.dart';
    
    class UsersScreen extends StatefulWidget {
      static const String id = 'users_screen';
    
      @override
      _UsersScreenState createState() => _UsersScreenState();
    }
    
    class _UsersScreenState extends State<UsersScreen> {
      static Map<String, dynamic> userDetails = {};
      static final String environment = userDetails['environment'];
      Widget appBarTitle = Text('Manage all users');
      Icon actionIcon = Icon(Icons.search);
      final TextEditingController _controller = TextEditingController();
      String approved = 'yes';
    
      getData() async {
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
        return await _firestore
            .collection('users')
            .document(user.uid)
            .get()
            .then((val) {
          userDetails.addAll(val.data);
        }).whenComplete(() {
          print('${userDetails['environment']}');
          setState(() {});
        });
      }
    
      _printLatestValue() {
        print('value from searchfield: ${_controller.text}');
      }
    
      @override
      void initState() {
        getData();
        _controller.addListener(_printLatestValue);
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
              title: appBarTitle,
              actions: <Widget>[
                IconButton(
                  icon: actionIcon,
                  onPressed: () {
                    setState(() {
                      if (this.actionIcon.icon == Icons.search) {
                        this.actionIcon = Icon(Icons.close);
                        this.appBarTitle = TextField(
                          controller: _controller,
                          style: TextStyle(
                            color: Colors.white,
                          ),
                          decoration: InputDecoration(
                              prefixIcon: Icon(Icons.search, color: Colors.white),
                              hintText: "Search...",
                              hintStyle: TextStyle(color: Colors.white)),
                          onChanged: (value) {
                            //do something
                          },
                        );
                      } else {
                        this.actionIcon = Icon(Icons.search);
                        this.appBarTitle = Text('Manage all users');
                        // go back to showing all users
                      }
                    });
                  },
                ),
              ]),
          body: SafeArea(
            child: StreamUsersList('${userDetails['environment']}', approved),
          ),
        );
      }
    }
    
    class StreamUsersList extends StatelessWidget {
      final String environmentName;
      final String approved;
      StreamUsersList(this.environmentName, this.approved);
      static String dropdownSelected2 = '';
    
      @override
      Widget build(BuildContext context) {
        return StreamBuilder<QuerySnapshot>(
            stream: Firestore.instance
                .collection('users')
                .where('environment', isEqualTo: environmentName)
                .where('approved', isEqualTo: approved)
                .snapshots(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return Center(
                  child: CircularProgressIndicator(
                    backgroundColor: Colors.lightBlueAccent,
                  ),
                );
              } else if (snapshot.connectionState == ConnectionState.done &&
                  !snapshot.hasData) {
                return Center(
                  child: Text('No users found'),
                );
              } else if (snapshot.hasData) {
                return ListView.builder(
                    padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
                    itemCount: snapshot.data.documents.length,
                    itemBuilder: (BuildContext context, int index) {
                      DocumentSnapshot user = snapshot.data.documents[index];
                      return Padding(
                        padding: EdgeInsets.symmetric(
                          horizontal: 7.0,
                          vertical: 3.0,
                        ),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                    //This CardCustom is just a Card with some styling
                            CardCustomUsers(
                                title: user.data['unit'],
                                weight: FontWeight.bold,
                                subTitle:
                                    '${user.data['name']} - ${user.data['login']}',
                            ),
                          ],
                        ),
                      );
                    });
              } else {
                return Center(
                  child: Text('Something is wrong'),
                );
              }
            });
      }
    }
    

    EDITED

    I managed to implement the search functionality in a simpler way, without having to change much of my code. For other beginners I have included the code below:

    Inside my _UsersScreenState I added String searchResult = ''; below my other variables. I then changed the onChanged of the TextField to:

    onChanged: (String value) {
                            setState(() {
                              searchResult = value;
                            });
                          },```
    

    I passed this on to the StreamUsersList and added it in the initialization. And in the ListView.Builder I added an if-statement with (snapshot.data.documents[index].data['login'].contains(searchResult)). See the below code of my ListView.Builder for an example.

    else if (snapshot.hasData) {
                return ListView.builder(
                    padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
                    itemCount: snapshot.data.documents.length,
                    itemBuilder: (BuildContext context, int index) {
                      DocumentSnapshot user = snapshot.data.documents[index];
                      final record3 = Record3.fromSnapshot(user);
                      String unitNr = user.data['unit'];
                      if (user.data['login'].contains(searchResult) ||
                          user.data['name'].contains(searchResult) ||
                          user.data['unit'].contains(searchResult)) {
                        return Padding(
                          padding: EdgeInsets.symmetric(
                            horizontal: 7.0,
                            vertical: 3.0,
                          ),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
    //This CardCustom is just a Card with some styling
                               CardCustomUsers(
                                  title: unitNr,
                                  color: Colors.white,
                                  weight: FontWeight.bold,
                                  subTitle:
                                      '${user.data['name']}\n${user.data['login']}',
                              ),
                            ],
                          ),
                        );
                      } else {
                        return Visibility(
                          visible: false,
                          child: Text(
                            'no match',
                            style: TextStyle(fontSize: 4.0),
                          ),
                        );
                      }
                    });
              } else {
                return Center(
                  child: Text('Something is wrong'),
                );
              }