Create infinite list with Cloud Firestore in flutter - dart

I am currently using Cloud Firestore with the Streambuilder widget in order to populate a ListView widget with Firestore documents.
new StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('videos').limit(10).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Center(
child: new CircularProgressIndicator(),
);
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
new Card(child: ...)
}).toList(),
);
},
);
This setup however only allows for the querying of the first x results (in this case x=10), with x being a fixed number that will sooner or later be exceeded by the number of Card widgets the user wants to see as he or she scrolls down.
Would it now be possible to query the first x results, and after the user hits a scroll threshold to query the next x+10 results from Cloud Firestore and so on?
This would allow for a dynamic list length which would also benefit the Firestore data consumption.

I am not sure whether it is possible or not with Streambuilder. I have integrated the similar functionality in my App using the startAfter method as shown below
class Feed extends StatefulWidget {
Feed({this.firestore});
final Firestore firestore;
#override
_FeedState createState() => _FeedState();
}
class _FeedState extends State<Feed> {
ScrollController controller;
DocumentSnapshot _lastVisible;
bool _isLoading;
CollectionReference get homeFeeds => widget.firestore.collection('homefeed');
List<DocumentSnapshot> _data = new List<DocumentSnapshot>();
final scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
controller = new ScrollController()..addListener(_scrollListener);
super.initState();
_isLoading = true;
_getData();
}
Future<Null> _getData() async {
// await new Future.delayed(new Duration(seconds: 5));
QuerySnapshot data;
if (_lastVisible == null)
data = await widget.firestore
.collection('homefeed')
.orderBy('created_at', descending: true)
.limit(3)
.getDocuments();
else
data = await widget.firestore
.collection('homefeed')
.orderBy('created_at', descending: true)
.startAfter([_lastVisible['created_at']])
.limit(3)
.getDocuments();
if (data != null && data.documents.length > 0) {
_lastVisible = data.documents[data.documents.length - 1];
if (mounted) {
setState(() {
_isLoading = false;
_data.addAll(data.documents);
});
}
} else {
setState(() => _isLoading = false);
scaffoldKey.currentState?.showSnackBar(
SnackBar(
content: Text('No more posts!'),
),
);
}
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
appBar: new AppBar(),
body: RefreshIndicator(
child: ListView.builder(
controller: controller,
itemCount: _data.length + 1,
itemBuilder: (_, int index) {
if (index < _data.length) {
final DocumentSnapshot document = _data[index];
return new Container(
height: 200.0,
child: new Text(document['question']),
);
}
return Center(
child: new Opacity(
opacity: _isLoading ? 1.0 : 0.0,
child: new SizedBox(
width: 32.0,
height: 32.0,
child: new CircularProgressIndicator()),
),
);
},
),
onRefresh: ()async{
_data.clear();
_lastVisible=null;
await _getData();
},
),
);
}
#override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
void _scrollListener() {
if (!_isLoading) {
if (controller.position.pixels == controller.position.maxScrollExtent) {
setState(() => _isLoading = true);
_getData();
}
}
}
}
Hope it helps!

That's definitely possible, but there's nothing pre-built in the API.
You'll have to remember the last document on the first page, and then startAfter() with that document to get the second page of documents.
See the documentation on Paginating Data with Query Cursors.

Related

How to rebuild all grid items in flutter?

I have a dashboard, represented by grid, that supposed to delete item on long press event (using flutter_bloc), but it deletes last item instead of selected. All debug prints show, that needed element actually removed from list, but view layer still keeps it.
My build function code:
Widget build(BuildContext context) {
double pyxelRatio = MediaQuery.of(context).devicePixelRatio;
double width = MediaQuery.of(context).size.width * pyxelRatio;
return BlocProvider(
bloc: _bloc,
child: BlocBuilder<Request, DataState>(
bloc: _bloc,
builder: (context, state) {
if (state is EmptyDataState) {
print("Uninit");
return Center(
child: CircularProgressIndicator(),
);
}
if (state is ErrorDataState) {
print("Error");
return Center(
child: Text('Something went wrong..'),
);
}
if (state is LoadedDataState) {
print("empty: ${state.contracts.isEmpty}");
if (state.contracts.isEmpty) {
return Center(
child: Text('Nothing here!'),
);
} else{
print("items count: ${state.contracts.length}");
print("-------");
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite)print("fut:${state.contracts[i].name} id:${state.contracts[i].id}");
}
print("--------");
List<Widget> testList = new List<Widget>();
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
}
return GridView.count(
crossAxisCount: width >= 900 ? 2 : 1,
padding: const EdgeInsets.all(2.0),
children: testList
);
}
}
})
);
}
full class code and dashboard bloc
Looks like grid rebuilds itself, but don't rebuild its tiles.
How can I completely update grid widget with all its subwidgets?
p.s i've spent two days fixing it, pls help
I think you should use a GridView.builderconstructor to specify a build function which will update upon changes in the list of items, so when any update occur in your data the BlocBuilder will trigger the build function inside yourGridView.
I hope this example makes it more clear.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
List<int> testList = List<int>();
#override
void initState() {
for (int i = 0; i < 20; i++) {
testList.add(i);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
//Here we can remove an item from the list and using setState
//or BlocBuilder will rebuild the grid with the new list data
onPressed: () => setState(() {testList.removeLast();})
),
body: GridView.builder(
// You must specify the items count of your grid
itemCount: testList.length,
// You must use the GridDelegate to specify row item count
// and spacing between items
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.0,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
),
// Here you can build your desired widget which will rebuild
// upon changes using setState or BlocBuilder
itemBuilder: (BuildContext context, int index) {
return Text(
testList[index].toString(),
textScaleFactor: 1.3,
);
},
),
);
}
}
Your code is always sending the last value of int i.
So instead of
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
Do
List<Widget> testList = new List<Widget>();
state.contracts.forEach((contract){
if(contract.isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(contract.id));
},
onTap: onTap,
child:DashboardCardWidget(contract)
)
));
Is it actually rebuilds? I'm just don't understand why you use the State with BLoC. Even if you use the State you should call the setState() method to update the widget with new data.
On my opinion the best solution to you will be to try to inherit your widget from StatelessWidget and call the dispatch(new UpdateRequest()); in the DashBLOC constructor.
Also always keep in mind this link about the bloc, there are lots of examples:
https://felangel.github.io/bloc/#/
just give children a key
return GridView.builder(
itemCount: children.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(3),
itemBuilder: (context, index) {
return Container(
key: ValueKey(children.length+index),
);
});

Flutter how to update text on dropdownbutton when using Sqflite to populate the list

I have no problem populating the list from Sqflite database on DropdownButton. My only problem is updating the text once it's selected. It kept showing 'Airport' and I'm still learning to work with Object instead of String. I just couldn't figure that out.
Here's the code:
String selectedAirport;
AirportModel _currentAirport;
...
children: <Widget>[
FutureBuilder<List<AirportModel>>(
future: db.getAllAirports(),
builder: (BuildContext context, AsyncSnapshot<List<AirportModel>> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return DropdownButton<AirportModel>(
items: snapshot.data
.map((airportItem) =>
DropdownMenuItem<AirportModel>(
value: airportItem,
child: Text(airportItem.airportName),
))
.toList(),
onChanged: (AirportModel value) {
setState(() {
_currentAirport = value;
selectedAirport = _currentAirport.airportName;
});
},
hint: Text("Airport"),
);
}),
DropdownButton has a property value. use it like value=_currentAirport
return DropdownButton<AirportModel>(
value:_currentAirport,
items: snapshot.data
.map((airportItem) =>
DropdownMenuItem<AirportModel>(
value: airportItem,
child: Text(airportItem.airportName),
))
.toList(),
onChanged: (AirportModel value) {
setState(() {
_currentAirport = value;
selectedAirport = _currentAirport.airportName;
});
},
hint: Text("Airport"),
);
Maybe items didn't reach yet or empty when value is set to DropdownButton. is _currentAirport initialized to some other value already?
Can you try like this? Also check if the items list are empty
items: snapshot.data == null ? null : _currentAirport
You can declare a Future and init in initState and in FutureBuilder use this future.
AirportModel _currentAirport;;
Future _future;
#override
void initState() {
_future = db.getAllAirports();
super.initState();
}
body: FutureBuilder<List<AirportModel>>(
future: _future,
You can use stream builder. Please check the example below.
class DropDownMenu extends StatefulWidget {
#override
_DropDownMenuState createState() => _DropDownMenuState();
}
class _DropDownMenuState extends State<DropDownMenu> {
var _currentSelectedValue;
final _dbHelper = DatabaseHelper.instance;
LoginPageManager _loginPageManager = new LoginPageManager();
final ValueNotifier<List<DropdownMenuItem<String>>> _dropDownMenuItems =
ValueNotifier<List<DropdownMenuItem<String>>>([]);
#override
void initState() {
_updateList();
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
width: 300,
height: 50,
margin: const EdgeInsets.only(top: 00.0),
child: ValueListenableBuilder(
builder: (BuildContext context, List<DropdownMenuItem<String>> list,
Widget child) {
return Container(
child: DropdownButton<String>(
hint: Text("Please Select a Server"),
value: _currentSelectedValue,
onChanged: (value) {
setState(() {
_currentSelectedValue = value;
});
},
items: list),
);
},
valueListenable: _dropDownMenuItems,
),
);
}
_updateList() async {
print("Update server has been called");
_dropDownMenuItems.value.clear();
List<Map<String, dynamic>> x = await _dbHelper.queryAllRows();
_dropDownMenuItems.value.add(_getAddServerButton());
x.forEach((element) {
_dropDownMenuItems.value.add(_getDropDownWidget(element));
});
}
DropdownMenuItem<String> _getDropDownWidget(Map<String, dynamic> map) {
int id = map['yxz'];
String text =
map['xyz'];
String value = map['zyx'];
return DropdownMenuItem<String>(
value: value,
child: Container(
width: 270,
child: Row(
children: [_getText(text), _getRemoveButton(id), _getEditButton(id)],
),
));
}
}
To make sure api data is not null:
child: _identity1 != null
? DropdownButtonFormField<dynamic>(
validator: (value) => value == null ? 'field required' : null

Future.wait() for multiple futures

I'm trying to catch the error when my device has no internet connection. I've built out 2 future methods, 1 to import a json and 1 to look into the database. I have a future builder that's suppose to wait for both futures to finish before building out the grid view but it seems like the offlineFlashCardList is being prematurely called due to the connection error. Any idea how to make it wait for both futures to finish before the snapshot error gets called?
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:baby_sound/strings.dart';
import 'package:baby_sound/objects/flashCardList.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'dart:async' show Future;
import 'dart:convert';
import 'package:baby_sound/database/database.dart';
import 'package:baby_sound/objects/network.dart';
import 'package:http/http.dart' as http;
class GridViewWidget extends StatefulWidget{
#override
createState() => new GridViewState();
}
class GridViewState extends State<GridViewWidget>{
List<FlashCardList> flashCardList;
List<FlashCardList> offlineFlashCardList;
Future<List<FlashCardList>> fetchFlashCardList() async{
debugPrint("before response");
List<FlashCardList> tempFlashCardList;
final response = await http.get('some json url');
//checkConnection(url).then((response){
debugPrint ("after database load, response code: ${response.statusCode}");
if (response.statusCode == 200) {
var data = json.decode(response.body);
var flashCardListData = data["FlashCardList"] as List;
tempFlashCardList = flashCardListData.map<FlashCardList>((json) => FlashCardList.fromJson(json)).toList();
for (int i = 0; i < tempFlashCardList.length; i++){
debugPrint("DBProvider listID: ${await DBProvider.db.getFlashCardList(tempFlashCardList[i].flashCardListID)}, flashCardID: ${tempFlashCardList[i].flashCardListID}");
if (await DBProvider.db.getFlashCardList(tempFlashCardList[i].flashCardListID) == null){
DBProvider.db.newFlashCardList(tempFlashCardList[i]);
debugPrint("Adding ${tempFlashCardList[i].name}}}");
} else {
DBProvider.db.updateFlashCardList(tempFlashCardList[i]);
debugPrint("Updating ${tempFlashCardList[i].name}, getFlashCardList: ${DBProvider.db.getFlashCardList(tempFlashCardList[i].flashCardListID)}");
}
}
flashCardList = tempFlashCardList;
debugPrint("Standard flashCardList Size: ${flashCardList.length}");
}
debugPrint("flashCardList Size Before Return: ${flashCardList.length}");
return flashCardList;
}
Future<List<FlashCardList>> fetchFlashCardListFromDB() async{
offlineFlashCardList = await DBProvider.db.getAllFlashCardListFromDB();
debugPrint("fetchFromDB size: ${offlineFlashCardList.length}");
return offlineFlashCardList;
}
#override
void initState(){
debugPrint ('debug main.dart');
super.initState();
}
#override
Widget build(BuildContext context){
return new Scaffold(
appBar: new AppBar(
centerTitle: true,
title: new Text(Strings.pageTitle),
),
body: FutureBuilder<List<FlashCardList>>(
future: new Future(() async{
await fetchFlashCardList();
await fetchFlashCardListFromDB();
}),
builder: (BuildContext context, AsyncSnapshot<List<FlashCardList>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
debugPrint("Snapshot has error: ${snapshot.error}");
return new GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
childAspectRatio: 0.5),
itemCount: offlineFlashCardList.length,
itemBuilder: (BuildContext context, int index) {
return _getGridItemUI(context, offlineFlashCardList[index]);
});
// return new Center(child: new CircularProgressIndicator());
} else {
debugPrint("Grid ViewBuilder");
return new GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
childAspectRatio:0.5),
itemCount: flashCardList.length,
itemBuilder: (BuildContext context, int index) {
return _getGridItemUI(context, flashCardList[index]);
});
}
}else {
debugPrint("CircularProgress");
return new Center(child: new CircularProgressIndicator());
}
})
);
}
_getGridItemUI(BuildContext context, FlashCardList item){
return new InkWell(
onTap: () {
_showSnackBar(context, item);
},
child: new Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Image(image: new CachedNetworkImageProvider("https://babymozart.org/babymozartq92C9TLa9UMkulL2m81xHdn9u2R92e1e/image/" + item.image)),
/*new Expanded(
child:new Center(
child: new Column(
children: <Widget>[
new SizedBox(height: 8.0),
new Expanded(
child: AutoSizeText(
item.name, maxLines: 1,
)
)
],
),
)
)*/
],
),
elevation: 2.0,
margin: EdgeInsets.all(5.0),
)
);
}
_showSnackBar(BuildContext context, FlashCardList item){
}
}
You can use Future.wait to wait for several Future to be completed.
body: FutureBuilder<List<FlashCardList>>(
future: Future.wait([
fetchFlashCardList(),
fetchFlashCardListFromDB(),
]),
Here's an example based on Alexandre's answer (As I found myself looking for how to handle results):
FutureBuilder(
future: Future.wait([
firstFuture(), // Future<bool> firstFuture() async {...}
secondFuture(),// Future<bool> secondFuture() async {...}
//... More futures
]),
builder: (
context,
// List of booleans(results of all futures above)
AsyncSnapshot<List<bool>> snapshot,
){
// Check hasData once for all futures.
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
// Access first Future's data:
// snapshot.data[0]
// Access second Future's data:
// snapshot.data[1]
return Container();
}
);

Flutter: Is it Possbile to have Multiple Futurebuilder or A Futurebuilder for Multiple Future Methods?

So let's say I'm fetching different List from Different Url's
so The Future function will look like this
Future<List<City>> getCityDetails(Region selectedRegion) async {}
Future<List<Region>> getregionDetails() async {}
thus the Widget Builder will look like this
FutureBuilder<List<Region>>(
future: getregionDetails(),
builder: (context, snapshot) {}
is there a way or work around in this one? I'm trying to find an answer how to implement this one or is there another way?
i think you are looking for something like this
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MaterialApp(
home: MyApp(),
));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget createRegionsListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty
? Column(
children: <Widget>[
ListTile(
title: Text(values[index].region),
),
Divider(
height: 2.0,
),
],
)
: CircularProgressIndicator();
},
);
}
Widget createCountriesListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
selectedCountry = values[index].code;
});
print(values[index].code);
print(selectedCountry);
},
child: Column(
children: <Widget>[
new ListTile(
title: Text(values[index].name),
selected: values[index].code == selectedCountry,
),
Divider(
height: 2.0,
),
],
),
);
},
);
}
final String API_KEY = "03f6c3123ea549f334f2f67c61980983";
Future<List<Country>> getCountries() async {
final response = await http
.get('http://battuta.medunes.net/api/country/all/?key=$API_KEY');
if (response.statusCode == 200) {
var parsedCountryList = json.decode(response.body);
List<Country> countries = List<Country>();
parsedCountryList.forEach((country) {
countries.add(Country.fromJSON(country));
});
return countries;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load ');
}
}
Future<List<Region>> getRegions(String sl) async {
final response = await http
.get('http://battuta.medunes.net/api/region/$sl/all/?key=$API_KEY');
if (response.statusCode == 200) {
var parsedCountryList = json.decode(response.body);
List<Region> regions = List<Region>();
parsedCountryList.forEach((region) {
regions.add(Region.fromJSON(region));
});
return regions;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load ');
}
}
String selectedCountry = "AF";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Row(children: [
Expanded(
child: FutureBuilder(
future: getCountries(),
initialData: [],
builder: (context, snapshot) {
return createCountriesListView(context, snapshot);
}),
),
Expanded(
child: FutureBuilder(
future: getRegions(selectedCountry),
initialData: [],
builder: (context, snapshot) {
return createRegionsListView(context, snapshot);
}),
),
]),
);
}
}
class Country {
String name;
String code;
Country({this.name, this.code});
factory Country.fromJSON(Map<String, dynamic> json) {
return Country(
name: json['name'],
code: json['code'],
);
}
}
class Region {
String country;
String region;
Region({this.country, this.region});
factory Region.fromJSON(Map<String, dynamic> json) {
return Region(
region: json["region"],
country: json["country"],
);
}
}

Flutter: Firebase basic Query or Basic Search code

The main concept is showing documents or fields which contains the searched alphabet.
The search bar gets the given input, it send to the _firebasesearch(), but in return nothing comes out, and the above image is my database structure, trying to figure out more than a week.
CODE
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_search_bar/flutter_search_bar.dart';
SearchBar searchBar;
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
class DisplayCourse extends StatefulWidget {
#override
_DisplayCourseState createState() => new _DisplayCourseState();
}
AppBar _buildAppBar(BuildContext context) {
return new AppBar(
title: new Text("FIREBASE QUERY"),
centerTitle: true,
actions: <Widget>[
searchBar.getSearchAction(context),
],
);
}
class _DisplayCourseState extends State<DisplayCourse> {
String _queryText;
_DisplayCourseState() {
searchBar = new SearchBar(
onSubmitted: onSubmitted,
inBar: true,
buildDefaultAppBar: _buildAppBar,
setState: setState,
);
}
void onSubmitted(String value) {
setState(() {
_queryText = value;
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You have Searched something!'),
backgroundColor: Colors.yellow,
));
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
key: _scaffoldKey,
appBar: searchBar.build(context),
backgroundColor: Colors.red,
body: _fireSearch(_queryText),
);
}
}
Widget _fireSearch(String queryText) {
return new StreamBuilder(
stream: Firestore.instance
.collection('courses')
.where('title', isEqualTo: queryText)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_buildListItem(snapshot.data.documents[index]),
);
},
);
}
Widget _buildListItem(DocumentSnapshot document) {
return new ListTile(
title: document['title'],
subtitle: document['subtitle'],
);
}
the main concept is showing document sor fields which contains the searched alphabet
the search bar gets the given input, it send to the _firebasesearch(),but in return nothing comes out, and the above image is my database structure, trying to figure out more than a week,
This might sound a ridiculous solution but actually works so well, It's almost like the Like '%' query from SQL
In the TextField as you type a value the inside where() isGreaterThanOrEqualTowill compare it and all the string values greater than the input and If you concatinate a 'Z'
At the end then isLessThan will end just after your search keyword and You get the desired Result from firestore.
// Declare your searchkey and Stream variables first
String searchKey;
Stream streamQuery;
TextField(
onChanged: (value){
setState(() {
searchKey = value;
streamQuery = _firestore.collection('Col-Name')
.where('fieldName', isGreaterThanOrEqualTo: searchKey)
.where('fieldName', isLessThan: searchKey +'z')
.snapshots();
});
}),
I used this Stream in StreamBuilder and It works exactly as expected.
Limitations:
The search is case sensitive(You can convert searchKey to specific case if your data is consistent like Type Case )
You have to start searching from the first letter, it can't search from mid
I'm a bit too late but I just want to share something on how I implement the search function without using third party app in my case. My solution is a bit straight forward querying using firestore. Here's the code:
Future<List<DocumentSnapshot>> getSuggestion(String suggestion) =>
Firestore.instance
.collection('your-collection')
.orderBy('your-document')
.startAt([searchkey])
.endAt([searchkey + '\uf8ff'])
.getDocuments()
.then((snapshot) {
return snapshot.documents;
});
example if you want to search all keywords containing "ab" then it will display all words containing "ab" (ex. abcd, abde, abwe). If you want to make auto suggest search function you can use typehead. which can be found in this link: https://pub.dev/packages/flutter_typeahead
Good luck.
You don't have to rebuild your whole stream, just filter the results from your stream depending on your search string.
Fast, does not need to rebuild the whole stream, finds occurences of the search string not only from the start of a word and is case-insensitive.
return StreamBuilder(
stream: FirebaseFirestore.instance.collection("shops").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) // TODO: show alert
return Text('Something went wrong');
if (snapshot.connectionState == ConnectionState.waiting)
return Column(
children: [
Center(
child: CupertinoActivityIndicator()
)
],
);
var len = snapshot.data.docs.length;
if(len == 0)
return Column(
children: [
SizedBox(height: 100),
Center(
child: Text("No shops available", style: TextStyle(fontSize: 20, color: Colors.grey)),
)
],
);
List<Shop> shops = snapshot.data.docs.map((doc) => Shop(
shopID: doc['shopID'],
name: doc['name'],
...
)).toList();
shops = shops.where((s) => s.name.toLowerCase().contains(searchString.text.toLowerCase())).toList();
return
Expanded(
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15),
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: shops.length,
itemBuilder: (context, index) {
return shopRow(shops[index]);
}
),
);
},
);
The issue is you are expecting results from firestore where title is equal to queryText not title contains queryText.
If you want the search feature, you can get and store the firestore documents in a variable something like List<Model> model instead of StreamBuilder and implement search manually from the above stored list of model.
The solution that i found:
List<String> listaProcura = List();
String temp = "";
for(var i=0;i<nomeProduto.length; i++) {
if(nomeProduto[i] == " ") {
temp = "";
} else {
temp = temp + nomeProduto[i];
listaProcura.add(temp);
}
}
The "listaProcura" is the name of the list.
the String "temp" is the name of a temporary string.
This way you will save this list of names in the firebase database.
Will be like:
[0] E
[1] Ex
[2] Exa
[3] Exam
[4] Examp
[5] Exampl
[6] Example
[7] o
[8] on
[9] one
For retrieving this info with the word you wanna search:
await Firestore.instance.collection('name of your collection').where('name of your list saved in the firebase', arrayContains: 'the name you are searching').getDocuments();
This way if you search for "one" and the name is "Example one" the search will return properly.
THIS IS ANOTHER SEARCH CODE THIS WILL SEARCH INSIDE FIREBASE DATABASE
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_animated_list.dart';
class Db extends StatefulWidget {
#override
HomeState createState() => HomeState();
}
class HomeState extends State<Db> {
List<Item> Remedios = List();
Item item;
DatabaseReference itemRef;
TextEditingController controller = new TextEditingController();
String filter;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
#override
void initState() {
super.initState();
item = Item("", "");
final FirebaseDatabase database = FirebaseDatabase.instance; //Rather then just writing FirebaseDatabase(), get the instance.
itemRef = database.reference().child('Remedios');
itemRef.onChildAdded.listen(_onEntryAdded);
itemRef.onChildChanged.listen(_onEntryChanged);
controller.addListener(() {
setState(() {
filter = controller.text;
});
});
}
_onEntryAdded(Event event) {
setState(() {
Remedios.add(Item.fromSnapshot(event.snapshot));
});
}
_onEntryChanged(Event event) {
var old = Remedios.singleWhere((entry) {
return entry.key == event.snapshot.key;
});
setState(() {
Remedios\[Remedios.indexOf(old)\] = Item.fromSnapshot(event.snapshot);
});
}
void handleSubmit() {
final FormState form = formKey.currentState;
if (form.validate()) {
form.save();
form.reset();
itemRef.push().set(item.toJson());
}
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
centerTitle: true,
backgroundColor: new Color(0xFFE1564B),
),
resizeToAvoidBottomPadding: false,
body: Column(
children: <Widget>\[
new TextField(
decoration: new InputDecoration(
labelText: "Type something"
),
controller: controller,
),
Flexible(
child: FirebaseAnimatedList(
query: itemRef,
itemBuilder: (BuildContext context, DataSnapshot snapshot,
Animation<double> animation, int index) {
return Remedios\[index\].name.contains(filter) || Remedios\[index\].form.contains(filter) ? ListTile(
leading: Icon(Icons.message),
title: Text(Remedios\[index\].name),
subtitle: Text(Remedios\[index\].form),
) : new Container();
},
),
),
\],
),
);
}
}
class Item {
String key;
String form;
String name;
Item(this.form, this.name);
Item.fromSnapshot(DataSnapshot snapshot)
: key = snapshot.key,
form = snapshot.value\["form"\],
name = snapshot.value\["name"\];
toJson() {
return {
"form": form,
"name": name,
};
}
}
if Search list is case senstive like this :
Curaprox Be You Display
Curaprox Black is White Display
Curaprox Black is White Mini Display
Curaprox Hydrosonic Pro Display
Curaprox Large Interdental Brush Display
then :
response = await FirebaseFirestore.instance
.collection('pointOFSale')
.orderBy("title")
.startAt([val.capitalize()]).endAt(
[val[0].toUpperCase() + '\uf8ff']).get();
Extension code :
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
if List is like :
curaprox be you display
curaprox black is white display
curaprox black is white mini display
then :
response = await FirebaseFirestore.instance
.collection('pointOFSale')
.orderBy("title")
.startAt([val]).endAt([val + '\uf8ff']).get();
soo simple and fast.
if (text.length > 1) {
setState(() {
tempSearchStore = _listPkh.documents.where((d) {
if (d['nama'].toLowerCase().indexOf(text) > -1) {
return true;
} else if (d['alamat'].toLowerCase().indexOf(text) > -1) {
return true;
}
return false;
}).toList();
});
} else {
setState(() {
tempSearchStore = _listPkh.documents;
});
}

Resources