Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

show full list by default if no search value #40

Open
davidchan666999 opened this issue Jul 25, 2020 · 7 comments
Open

show full list by default if no search value #40

davidchan666999 opened this issue Jul 25, 2020 · 7 comments

Comments

@davidchan666999
Copy link

thanks for the great package, is possible to show full list by default when no search value? It will be great if supported this option.

@asafeca
Copy link

asafeca commented Jul 28, 2020

Hey, set minimumChars to 0 (Zero); and on your search method whenever the value is empty you should return the entire list;

@davidchan666999
Copy link
Author

@CoderAware Thanks for your reply, minimumChars set to 0 is ok when i type some search text and then delete it, but still got some problem? 1. cannot show the list when initialization since "onSearch" event will not fire when i enter the screen. 2. When click Cancel, no way to set search value to empty and trigger Search function

Can we allow set search value and trigger search function via searchController? then we can call it when initState and onCancel, to make it more flexible? Or any other way can achieve my gold?
Thanks guys

@nea
Copy link

nea commented Aug 18, 2020

Hey @davidchan666999

You can set the placeHolder to a full e.g. _build() function.

Best

@wh120
Copy link

wh120 commented Aug 27, 2020

Hi
I am also need initial List int flappy
because placeHolder doesn't work if minimumChars = 0

@pelanmar1
Copy link

pelanmar1 commented Nov 30, 2020

I managed to solve this by rebuilding the whole widget when the async function that gets my initial full list finishes.

  1. Inside your State Class (you need a stateful widget) add a boolean flag and an empty list. Then, override the initState method and inside execute the async function that gets your full list (in my case, I call my search function with an empty string). When the function finishes, set your boolean flag to true using the setState logic (this tells the widget to rebuild).
class _TransactionsPageState extends State<TransactionsPage> {
  List<Transaction> _defaultList = new List();
  bool isLoaded = false;

  @override
  void initState() {
    Future.delayed(Duration.zero).then((value) async {
      _defaultList = await _search("");
      setState(() {
        isLoaded = true;
      });
    });
    super.initState();
  }
...

  1. In your build() method, use the suggestions argument to pass in your initial list.
@override
  Widget build(BuildContext context) {
    return SearchBar<Transaction>(
      onSearch: _search,
      onItemFound: (transaction, index) => _buildListTile(transaction),
      minimumChars: 1,
      hintText: "Type something...",
      searchBarPadding: EdgeInsets.symmetric(horizontal: 10),
      headerPadding: EdgeInsets.symmetric(horizontal: 10),
      suggestions: _defaultList,
    );
  }

Good luck

@jstnwang0
Copy link

@pelanmar1 That works, but you won't be able to filter and sort those.

@sagreine
Copy link

Hey @davidchan666999

You can set the placeHolder to a full e.g. _build() function.

Best

Seconding this. Look at building, for instance, as streambuilder.

In the end I hacked a way to pagination and stream listening both during and outside of search. None of this is good, as I'm new to this, but I ended up doing something like the below for a toy project. This takes a lot of less than fun tracking & global variables, but I'm sure a more adept coder could do much better.

  1. have a local list as my direct data source, with several ways of updating it from firestore. use pagination, so pull in ~15 records initially, and set to listen to firestore with a limit of 5 records, where matching an array-contains query (a subset of every letter in order of my field I'm searching, a common approach for firestore text search). for my use case I only care when things are added, not updated nor removed, and only up to 1 thing will ever be in the process of being added while viewing this page
  2. use streambuilder for placeholder. To use pagination, on initState I set a controller to listen to this streambuilder and look for when we scroll to the bottom of it to pull in the next records. I pull those into my datasource
  3. when searching, re-pull the inital records into my local data source based on those updated search terms and listen to firestore for any updates to my search terms.
  4. use a NotificationListener<ScrollNotification> above the SearchBar and capture when I scroll to the bottom of the search results. When we get there, pull in the next page of results and put them into the local data source. Then, using the searchbarcontroller, replay the last search -> this for me is a simple function that basically says "pull in everything from the local data source".
  5. Along the way, I am listening to firestore and adding records that match to the local data. If I'm in the middle of a search, the function that adds data to the local data store will again tell SearchBar to replay the last search. If I'm not, simply inserting the new record at the front of the local data, and my listener sets State to refresh.

class OldVideosView extends StatefulWidget {
  @override
  OldVideosViewState createState() => OldVideosViewState();
}

class OldVideosViewState extends State<OldVideosView> {
  List<ExerciseSet> _videos; // = ;
  var scrollController = ScrollController();
  var _streamController = StreamController<List<ExerciseSet>>.broadcast();
  QuerySnapshot collectionState;
  bool _isRequesting = false;
  String userId;
  var numDocumentsToPaginateNext = 5;
  bool gottenLastDocument = false;
  String search = "";
  bool getDocsInStreamBuilder;
  final SearchBarController<ExerciseSet> _searchBarController =
      SearchBarController();

  @override
  void initState() {
    super.initState();
    // set up the non-search scroll listner to paginate
    scrollController.addListener(() {
      if (scrollController.position.atEdge) {
        if (scrollController.position.pixels == 0)
          print('ListView scroll at top');
        else {
          print('ListView scroll at bottom');
          getDocumentsNext(context, search).then((value) {
            // Load next documents
            setState(() {});
          });
        }
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    scrollController.dispose();
    //_streamController.close();
  }

// omitted for irrelevance 
Container _shareBox(ExerciseSet video) {
    return Container()
}
// omitted for irrelevance 
_buildListItem(ExerciseSet video) {
    return Container()
}

  // listen to newly added items - this might be stupid because of where it puts it in the array...
  void onChangeData(List<DocumentChange> documentChanges) {
    documentChanges.forEach((productChange) {
      if (productChange.type == DocumentChangeType.added) {
        // check if we have already pulled this video -- hmmm shure should define equality in the model eh
        var temp = List<String>.from(productChange.doc.data()['keywords']);
        // if this is not a search, add it. otherwise, if it is a search and the item is one of our desired search results,
        // it is not yet ineligible to add
        bool searchCheck = search == "" || temp.contains(search);

        int indexWhere = _videos.indexWhere((video) {
          return (productChange.doc.data()['title'] == video.title &&
              productChange.doc.data()['reps'] == video.reps &&
              productChange.doc.data()['weight'] == video.weight &&
              DateTime.parse(productChange.doc.data()['dateTime']) ==
                  video.dateTime);
        });
        // if we haven't, add it to the list at the start so it shows up on top unless this is a search result and this item
        // does not match our search criteria
        if (indexWhere == -1 && searchCheck) {
          _videos.insert(
              0,
              ExerciseSet(
                videoPath: productChange.doc.data()['videoPath'],
                thumbnailPath: productChange.doc.data()['thumbnailPath'],
                aspectRatio: productChange.doc.data()['aspectRatio'],
                title: productChange.doc.data()['title'],
                reps: productChange.doc.data()['reps'],
                weight: productChange.doc.data()['weight'],
                dateTime:
                    DateTime.parse(productChange.doc.data()['dateTime']) ??
                        DateTime.now(),
              ));
          _streamController.add(_videos);
          // below lets us do "streaming" to search results
          if (search != "") {
            _searchBarController.replayLastSearch();
          }
        }
      }
    });
  }

  StreamBuilder<List<ExerciseSet>> _getStreamBuilder() {
    return StreamBuilder<List<ExerciseSet>>(
        stream: _streamController.stream,
        builder:
            (BuildContext context, AsyncSnapshot<List<ExerciseSet>> snapshot) {
          if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
          switch (snapshot.connectionState) {
            case ConnectionState.waiting:
              // on initializing the page OR after clearing a search, go get starter documents for this streambuilder to build with
              if (getDocsInStreamBuilder ?? true) {
                getDocsInStreamBuilder = false;
                // clear this to be sure here (if they delete, not cancel, out of search)
                search = "";
                getDocuments(context, "");
                print("get documents from streambuilder");
              }
              return new Text('Loading...');
            default:
              return ListView.builder(
                  shrinkWrap: true,
                  physics: AlwaysScrollableScrollPhysics(),
                  controller: scrollController,
                  itemCount: snapshot.data.length,
                  itemBuilder: (context, index) {
                    return _buildListItem(_videos[index]);
                  });
          }
        });
  }

  Future<List<ExerciseSet>> getDocuments(BuildContext context, String search,
      {bool firstLoad}) async {
    // this is a refresh of sorts, so clear our our cache of videos and that we've gotten all videos.
    _videos = <ExerciseSet>[];
    gottenLastDocument = false;
    // go get a 15 sample, most recent first
    var userId = (Provider.of<Muser>(context, listen: false)).fAuthUser.uid;
    var collection = FirebaseFirestore.instance
        .collection('/USERDATA/$userId/LIFTS')
        .where('keywords', arrayContains: search.toLowerCase())
        .orderBy("dateTime", descending: true)
        .limit(numDocumentsToPaginateNext + 10);
    print('getDocuments');

    await fetchDocuments(collection);
    // then, listen but only to the latest change (addition, we'll define it in the function) - they'll unlikely to add more than 1 at a time
    FirebaseFirestore.instance
        .collection('/USERDATA/$userId/LIFTS')
        .where('keywords', arrayContains: search.toLowerCase())
        .orderBy("dateTime", descending: true)
        .limit(1)
        .snapshots()
        .listen((data) => onChangeData(data.docChanges));
    _streamController.add(_videos);
    // this line may not actually be necessary since we're using a global..
    return _videos;
  }

  // get the next page
  Future<void> getDocumentsNext(BuildContext context, String search) async {
    if (_isRequesting == false && gottenLastDocument == false) {
      _isRequesting = true;
      // Get the last pulled document and go from there
      var userId = (Provider.of<Muser>(context, listen: false)).fAuthUser.uid;
      var lastVisible = collectionState.docs[collectionState.docs.length - 1];
      print('listDocument legnth: ${collectionState.size} last: $lastVisible');
      var collection = FirebaseFirestore.instance
          .collection('/USERDATA/$userId/LIFTS')
          .where('keywords', arrayContains: search.toLowerCase())
          .orderBy("dateTime", descending: true)
          .startAfterDocument(lastVisible)
          .limit(numDocumentsToPaginateNext);

      await fetchDocuments(collection);
      _streamController.add(_videos);
      _isRequesting = false;
    }
  }

  fetchDocuments(Query collection) async {
    await collection.get().then((value) {
      collectionState =
          value; // store collection state to set where to start next
      value.docs.forEach((element) {
        _videos.add(ExerciseSet(
          videoPath: element.data()['videoPath'],
          thumbnailPath: element.data()['thumbnailPath'],
          aspectRatio: element.data()['aspectRatio'],
          title: element.data()['title'],
          reps: element.data()['reps'],
          weight: element.data()['weight'],
          dateTime:
              DateTime.parse(element.data()['dateTime']) ?? DateTime.now(),
        ));
      });
      // if we have gotten the last document, say so, in order to not just query indefinitely.
      gottenLastDocument = value.docs.length < numDocumentsToPaginateNext;
    });
  }

  // TODO: make this search by anything instead of just title? or even e.g. "TITLE:Squat"
  // TODO: may yet be
  Future<List<ExerciseSet>> _getSearchResults(String text) async {
    getDocsInStreamBuilder = true;
    // if search is null, we'll go get documents - might be ablet o get rid of this as i'm not sure it ever gets called.
    if (text == "") {
      search = text;
      await getDocuments(context, text);
      setState(() {});
      return _videos;
    }
    // if ths is the same search term, don't do anything.
    else if (search == text) {
    }
    // if this is a new term to search, we need to repull initial values. this definitely needs to be here
    else {
      search = text;
      await getDocuments(context, text);
    }
    // might be able to put this in the conditional block above
    setState(() {});
    return _videos;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ReusableWidgets.getAppBar(),
      drawer: ReusableWidgets.getDrawer(context),
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Flexible(
              fit: FlexFit.loose,
              child: NotificationListener<ScrollNotification>(
                onNotification: (notification) {
                  // if we're searching and have hit the bottom index.
                  if (search != "") {
                    if (notification.metrics.atEdge) {
                      if (notification.metrics.pixels != 0) {
                        var temp = _videos.length;
                        print(
                            "number of search results before getting next page: ${_videos.length}");
                        getDocumentsNext(context, search).then((value) {
                          print(
                              "number of search results after getting next page: ${_videos.length}");
                          if (temp != _videos.length) {
                            // we've updated the source we're pulling from, so the same search will give more data
                            _searchBarController.replayLastSearch();
                          }
                        });
                      }
                    }
                  }
                  return true;
                },
                child: SearchBar<ExerciseSet>(
                  searchBarPadding: EdgeInsets.symmetric(horizontal: 10),
                  headerPadding: EdgeInsets.symmetric(horizontal: 10),
                  listPadding: EdgeInsets.symmetric(horizontal: 10),
                  onSearch: _getSearchResults,
                  searchBarController: _searchBarController,
                  minimumChars: 1,
                  placeHolder: _getStreamBuilder(),
                  cancellationWidget: Text("Cancel"),
                  emptyWidget: Text("None"),
                  // could put buttons here and _searchBarController.filter or otherwise modify the search field....
                  header: Row(
                    children: <Widget>[],
                  ),
                  onCancelled: () async {
                    search = "";
                    getDocsInStreamBuilder = true;
                  },
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  onItemFound: (exercise, int index) {
                    return _buildListItem(exercise);
                  },
                ),
              )),
        ],
      ),
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants