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

拟墨画扇 提交于 2020-06-16 03:46:13

问题


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'),
            );
          }

回答1:


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';
  }
}


来源:https://stackoverflow.com/questions/59397792/flutter-implementing-a-search-feature-for-data-from-a-streambuilder-with-listvi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!