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

1

6

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'),
            );
          }
Audra answered 18/12, 2019 at 18:3 Comment(1)
You should put your edit as your answerBouillabaisse
P
6

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';
  }
}
Polyethylene answered 18/12, 2019 at 18:25 Comment(11)
Thanks for your fast reply! Your structure makes sense when I read it however, I would have no clue how to implement this in my code! I will do some research on a ValueListenableBuilder, if you would happen to have an example of it feel free to share ;) Thanks again!Audra
I have a demo for you: dartpad.dartlang.org/32629ed5d1597af4460946ed3540d0c0Polyethylene
From this link you can run my demo app on dart pad --> click on the fab button --> It will open the modalBottomSheet --> try changing color from the bottom pallet. the change in background is happening because of the ValueNotifier and ValueListenableBuilderPolyethylene
If this answer helps you then don't forget to accept and up-vote it.Polyethylene
It's amazing that you did that so quick! I will take a look at the dart pad but since I'm not that good it will take a while for me to figure out how to apply it to my code and get it working. If it does I will accept it as the answer of course. Thanks!Audra
Okay, you can let me know if you have any doubts.Polyethylene
I'm not able to figure it out yet with the provided dartpad. I appreciate the time taken already to help me out, but maybe you could alter it a bit to reflect my case with the streambuilder and all? Please keep in mind that I am still a beginner with Dart and Flutter. Thanks anyway!Audra
I have updated my answer, I hope it helps. in case of any doubts please let me know. if this helps you solve your problem then please up-vote and accept the answer.Polyethylene
I feel like I'm getting close thanks to your help! I have implemented the code as suggested by you and edited my answer above. However, the second StreamBuilder doesn't seem to be returning any data. Did I do it all wrong?Audra
Yes, you have not assigned any value to filtered list in _filter() and that is why you are not receiving any data in your second stream builderPolyethylene
Add some thing like this: _dataFromQuerySnapShot .where((User user) => user.name.toLowerCase().contains(searchQuery.toLowerCase())) .toList();Polyethylene

© 2022 - 2024 — McMap. All rights reserved.