Open davidchan666999 opened 4 years ago
Hey, set minimumChars to 0 (Zero); and on your search method whenever the value is empty you should return the entire list;
@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
Hey @davidchan666999
You can set the placeHolder
to a full e.g. _build()
function.
Best
Hi I am also need initial List int flappy because placeHolder doesn't work if minimumChars = 0
I managed to solve this by rebuilding the whole widget when the async function that gets my initial full list finishes.
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();
}
...
2. In your build() method, use the suggestions argument to pass in your initial list.
@override
Widget build(BuildContext context) {
return SearchBar
Good luck
@pelanmar1 That works, but you won't be able to filter and sort those.
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.
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".
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);
},
),
)),
],
),
);
}
}
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.