Flutter: Firebase basic Query or Basic Search code - firebase-realtime-database

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

Related

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

Bloc Pattern to hide and unhide Widgets

I am trying to fetch some data from the internet and show it int a list.
Following is my bloc code
class StudentsBloc {
final _repository = Repository();
final _students = BehaviorSubject<StudentModel>();
final BehaviorSubject<bool> _showProgress = BehaviorSubject<bool>();
final BehaviorSubject<bool> _showNoInternetViews = BehaviorSubject<bool>();
Observable<StudentModel> get students => _students.stream;
Observable<bool> get showProgress => _showProgress.stream;
Observable<bool> get showNoInternetViews => _showNoInternetViews.stream;
//FetchStudent from my Api
fetchStudents(String disciplineId, String schoolId, String year_id,
String lastIndex) async {
final student = await _repository.fetchStudents(
disciplineId, schoolId, year_id, lastIndex);
_students.sink.add(student);
}
//Check to see if user has internet or not
isNetworkAvailable(String disciplineId, String schoolId, String year_id,
String lastIndex) async {
checkInternetConnection().then((isAvailable) {
if (isAvailable) {
fetchStudents(disciplineId, schoolId, year_id, lastIndex);
} else {
_students.sink.addError(NO_NETWORK_AVAILABLE);
}
});
}
Function(bool) get changeVisibilityOfProgress => _showProgress.sink.add;
Function(bool) get changeVisibilityOfNoInternetViews =>
_showNoInternetViews.sink.add;
dispose() {
_students.close();
_showProgress.close();
_showNoInternetViews.close();
}
}
Following is my main code to hide unide Widgets
Widget buildList(StudentsBloc bloc) {
return StreamBuilder(
stream: bloc.students,
builder: (context, AsyncSnapshot<StudentModel> snapshot) {
if (snapshot.hasError) {
bloc.changeVisibilityOfProgress(false);
bloc.changeVisibilityOfNoInternetViews(true);
return StreamBuilder(
stream: bloc.showNoInternetViews,
builder: (context, snapshot) {
bool showNoInternetView = snapshot.hasData ?? false;
return Visibility(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("No Network Available"),
RaisedButton(
onPressed: () {
fetchStudents();
},
child: Text("Retry"),
)
],
),
),
visible: showNoInternetView ? true : false,
);
},
);
}
if (snapshot.hasData) {
bloc.changeVisibilityOfProgress(false);
bloc.changeVisibilityOfNoInternetViews(false);
return Refresh(
year_id: "2",
schoolId: "1",
lastIndex: "0",
disciplineId: "1",
child: ListView.builder(
itemBuilder: (context, int index) {
return buildTile(
snapshot.data.toBuilder().data.studentData[index]);
},
itemCount: snapshot.data.toBuilder().data.studentData.length,
),
);
}
if (!snapshot.hasData) {
return StreamBuilder(
builder: (context, snapshot) {
bool showProgressIndicator = snapshot.data ?? false;
return Visibility(
child: Center(
child: CircularProgressIndicator(),
),
visible: showProgressIndicator ? true : false,
);
},
stream: bloc.showProgress,
);
}
},
);
}
The buildList method is called in the body of Scaffold
void fetchStudents() {
bloc?.changeVisibilityOfNoInternetViews(false);
bloc?.changeVisibilityOfProgress(true);
bloc?.isNetworkAvailable("1", "1", "2", "0");
}
Suppose the user has internet when app is opened then i am able to see circularprogressindicator and then the list of data is visible
but suppose at the start when app is opened and the user does not have internet then i am showing the No Network Available Text and a button to retry,
now if the user has connected to the internet and then click on button to retry i am directly seeing the list of data after few seconds instead of the circularprogressindicator.I am not able to understand why the NoInternetviews are not hiding and progressindicator is showing when retry button is clicked before showing list of data.
My stream is not getting updated on retry button called. Are there any caveats for StreamBuilder within StreamBuilder?
I tried changing the StreamBuilder order as mentioned by #ivenxu in the answer but it still does not work.
Following are the links of attached code
https://drive.google.com/file/d/15Z8jXw1OpwTB1CxDS8sHz8jKyHhLwJp7/view?usp=sharing
https://drive.google.com/open?id=1gIXV20S1o5jYRnno_NADabuIj4w163fF
in view layer you can use Visibility() widget and pass visible parameter true or false when load data from Internet.
let's think about how to change the visible variable from bloc.
The parent of Visibility() widget the StreamBuilder() to stream on changes data.
for your case you need a PublishSubject inside your bloc to stream and add new data, and Observable to stream on this data on your widget.
let's show a snippet code to help you how you can implement it
The bloc contains PublishSubject and Observable to stream and add data
//this Subject allows sending data, error and done events to the listener
final PublishSubject<bool> _progressStateSubject = new PublishSubject();
//the listener are streaming on changes
Observable<bool> get progressStateStream => _progressStateSubject.stream;
//to change your progress state
void changeProgressState({bool state}) => _progressStateSubject.sink.add(state);
Here you can change your state of your progress
void callWebService() async {
//when you call your func to fetch your data change your state to true
changeProgressState(state: true);
.
.
.
if(response != null){
//when your call is done change the state to false
changeProgressState(state: false);
}
}
Your progress widget is
// Loading Widget
Widget _buildLoadingWidget(Bloc bloc) {
return StreamBuilder<bool>(
stream: bloc.progressStateStream,//streaming on changes
builder: (context, snapshot) {
return Visibility(
visible: snapshot.data ?? false,//calling when data changes
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Loading data from API...",
textDirection: TextDirection.ltr),
CircularProgressIndicator()
],
),
),
);
});
}
It seems the cause is that CircularProgressIndicator is inside the students stream's update cycle. And the student steam only get next snapshot when the data returned from internet call in the case of retry.
Have a try on changing the order of stream builders, try putting the student streambuilder inside of the progress stream builder.
Widget buildList(StudentsBloc bloc) {
return StreamBuilder(
stream: bloc.showProgress,
builder: (context, snapshot) {
bool showProgressIndicator = snapshot.data ?? false;
if (!showProgressIndicator) {
StreamBuilder(
stream: bloc.students,
builder: (context, AsyncSnapshot<StudentModel> snapshot) {
....
//your original code without progress StreamBuilder
}
}
return Visibility(
child: Center(
child: CircularProgressIndicator(),
),
visible: showProgressIndicator ? true : false,
);
},
);
}

Flutter - Pass a Future List to a SearchDelegate

I've been following a Flutter Search tutorial on the - boring flutter show
. I have been trying to implement the same functionality using a list which is derived from a Future List where the data comes from an api (in this case and Aqueduct server).
Currently my screen lists all the contacts from the api, i'd now like to search against that contacts list. I'm assuming it would be best practice to pass the same list (which is already being displayed) to the search delegate. Unfortunately i'm not sure how to achieve this.
My code is as follows (please note i've stripped down some of the code for this examples):
class _ContactsScreenState extends State<ContactsScreen> {
static const _usersUrl = //url info;
static final _token = //token info;
static final _headers = {
"Content-type" : "application/json",
"Accept": "application/json",
"Authorization": "Bearer $_token",
};
HttpRequestStatus httpRequestStatus = HttpRequestStatus.NOT_DONE;
Future<List<Contact>> readContacts() async {
final response = await http.get(_usersUrl, headers: _headers);
List responseJson = json.decode(response.body.toString());
List<Contact> contactList = createContactList(responseJson);
return contactList;
}
List<Contact> createContactList(List data) {
List<Contact> list = List();
for (int i = 0; i < data.length; i++) {
String name = data[i]["name"];
int id = data[i]["id"];
Contact user = Contact(name: name, id: id);
list.add(user);
}
return list;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List Contacts'),
actions: [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: (){
showSearch(
context: context,
delegate: ContactSearch(),
);
}
),
],
),
body: Container(
child: FutureBuilder<List<Contact>>(
future: readContacts(),
builder: (context, snapshot) {
if (snapshot.hasData) {
//Code which displays the data (works fine);
}
}
),
)
)
}
}
class ContactSearch extends SearchDelegate<Contact> {
#override
<Widget> buildActions(BuildContext context){
//
}
#override
Widget buildLeading(BuildContext context){
//
}
#override
Widget buildSuggestions(BuildContext context){
//Pass contacts list to here and compares agains 'query'
}
#override
Widget buildResults(BuildContext context) {
return container();
}
}
So in brief, i need to pass the correct list/data through 'ContactSearch()':
showSearch(
context: context,
delegate: ContactSearch(),
);
Thanks in advance.
Basically you have to move the FutureBuilder further up the widget hierarchy, so both the search box as well as the body are below it. Then you can simply push your data into the ContactSearch.
For Example:
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Contact>>(
future: readContacts(),
builder: (context, snapshot) {
return Scaffold(
appBar: AppBar(
title: Text('List Contacts'),
actions: [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: !snapshot.hasData ? null : () {
showSearch(
context: context,
delegate: ContactSearch(snapshot.data),
);
}
),
],
),
body: Container(
child:
(snapshot.hasData ?
//Code which displays the data (works fine);
: /* show errors/progress/etc. */),
),
);
}
);
}

Create infinite list with Cloud Firestore in flutter

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.

screen not re-rendering after changing state

I'm just starting with Flutter, finished the first codelab and tried to add some simple functionality to it.
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
theme: new ThemeData(primaryColor: Colors.deepOrange),
home: new RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
#override
createState() => new RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = new TextStyle(fontSize: 18.0);
final _saved = new Set<WordPair>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.list),
onPressed: _pushSaved,
)
],
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if (i.isOdd) return new Divider();
final index = i ~/ 2;
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return _buildRow(_suggestions[index]);
},
);
}
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new IconButton(
icon: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border),
color: alreadySaved ? Colors.red : null,
onPressed: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
));
}
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return _buildRow(pair);
// new ListTile(
// title: new Text(
// pair.asPascalCase,
// style: _biggerFont,
// ),
// );
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}
}
In the Save suggestions screen I built the same row as in the Sugestions Screen.
In the Saved Sugstions screen when you click the heart icon the element is removed from the array of saved items but the screen is not re-rendered.
what am I doing wrong here?
thanks!
Update
Actually your app is working perfectly fine with me :/
Because you are not communicating the state change with the icon change. You are already changing state based on alreadySaved, notice how you managed setState()
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
In the previous block you are only removing or adding to your favourite list based on the boolean value of alreadySaved and you are not telling setState to change anything else. That is why the following does not produce a re-render even though alreadySaved is switching values
///These two lines do not know what is happening
icon: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border),
color: alreadySaved ? Colors.red : null,
So you can instead do the following
icon: new Icon(_whichIcon), //initialized var _whichIcon = Icons.favorite_border
color: _whichIconColor, //Initialized var _whichIconColor = Colors.transparent
And your setState would be:
setState(() {
if (alreadySaved) {
_saved.remove(pair);
_whichIcon = Icons.favorite_border ;
_whichIconColor = Colors.transparent;
} else {
_saved.add(pair);
_whichIcon = Icons.favorite ;
_whichIconColor = Colors.red;
}
});
Or simpler you can do it like this, and keep your icon logic unchanged:
bool alreadySaved = false;
...
setState(() {
if (_saved.contains(pair)) {
_saved.remove(pair);
alreadySaved = false;
} else {
_saved.add(pair);
alreadySaved = true;
}
});

Resources