I want to get the count of cached data of firestore whether i am offline or online in both conditions to differentiate new and old records in flutter android.
You can do that for example by using 2 FutureBuilders, using the parameter "source" from ".getDocuments(source: )", you can set Source.cache (retrieves data from the cache) or Source.server (retrieves data from the server).
FutureBuilder(
future: Firestore.instance.collection("yourCollection").getDocuments(source: Source.cache ),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data.documents.length == null) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Colors.white),
),
);
} else {
print("Count: ${snapshot.data.documents.length}");
return Container();
}
}
)
Related
I have a Future method which return type is
Future < List< Map< dynamic, dynamic>>>
In my WidgetBuilder, I would like to know the length of the snapshot but I don't know how to get the length:
: FutureBuilder(
future: getCollection(), // Return Future<List<Map<dynamic, dynamic>>>
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
//replace this with a loading icon
child: new CircularProgressIndicator());
} else {
return ListView.builder(
itemExtent: 90,
itemCount: snapshot. ? , <== How do I excess the length?
Assuming it's a list of Maps. I tried the following:
snapshot.data.data["someAttributeName"]
but it gave me a null exception. Any ideas how to get the length
If you set the type of your AsyncSnapshot, you can access its properties through autocompletion:
builder: (context, AsyncSnapshot<List<Map<dynamic, dynamic>>> snapshot)
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,
);
},
);
}
The getIngredients() method below returns a list from firestore.
Future getIngredients() async {
return Firestore.instance
.collection('ingredients')
.where("name", isEqualTo: widget.dish_name.toString().toLowerCase()).getDocuments();
}
Now I want to display this list in an item builder below:
new ListView.builder(
itemExtent: 90,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return SingleIngredient(
ingredient_name: snapshot.data[index].ingredients,
);
});
I get the following error message:
_FutureBuilderState#7cbda): I/flutter (12164): Class 'QuerySnapshot' has no instance getter 'length'. I/flutter (12164):
Receiver: Instance of 'QuerySnapshot' I/flutter (12164): Tried
calling: length
Here's the scturcture of my firestore. I am fetching the ingredients list:
UPDATE
I have updated the code but I am only getting the first item from the list of ingredients (i.e, Onions). I want the itembuilder to build each item in the list because I am trying to display an image and the the ingredient list. That's what the SingleIngredient widget is doing. So, how can I loop through each list one by one?
Widget build(BuildContext context) {
return Container(
child: FutureBuilder(
future: getIngredients(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot user = snapshot.data.documents[index];
return SingleIngredient(
// Access the fields as defined in FireStore
ingredient_name: user.data['ingredients'][index].toString(),
);
},
);
} else if (snapshot.connectionState == ConnectionState.done &&
!snapshot.hasData) {
// Handle no data
return Center(
child: Text("No users found."),
);
} else {
// Still loading
return CircularProgressIndicator();
}
}),
);
}
}
Here's an example using StreamBuilder where i retrieve all documents from a collection and build a ListView to show them:
Widget buildUserList(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot user = snapshot.data.documents[index];
return ListTile(
// Access the fields as defined in FireStore
title: Text(user.data['firstName']),
subtitle: Text(user.data['lastName']),
);
},
);
} else if (snapshot.connectionState == ConnectionState.done && !snapshot.hasData {
// Handle no data
return Center(
child: Text("No users found."),
);
} else {
// Still loading
return CircularProgressIndicator();
}
}
Usage:
Scaffold(
body: StreamBuilder(
stream:
Firestore.instance.collection('users').snapshots(),
builder: buildUserList,
)
)
or
Scaffold(
body: FutureBuilder(
future:
Firestore.instance.collection('users').getDocuments(),
builder: buildUserList,
)
)
Dart does not know what type you are returning from the Future so it's interpreting it as a dynamic object, which has no length getter on it. Change your method
Future getIngredients() async {
return Firestore.instance
.collection('ingredients')
.where("name", isEqualTo: widget.dish_name.toString().toLowerCase()).getDocuments();
}
To
Future<List<YourType>> getIngredients() async {
return Firestore.instance
.collection('ingredients')
.where("name", isEqualTo: widget.dish_name.toString().toLowerCase()).getDocuments();
}
Where YourType is the type that's returned from the getDocuments() function. You might need to do a toList() on it as well.
use FirestoreListView from flutterfire_ui package
final usersQuery = Firestore.instance
.collection('ingredients')
.where("name", isEqualTo: widget.dish_name.toString().toLowerCase());
FirestoreListView<Map<String, dynamic>>(
query: usersQuery,
itemBuilder: (context, snapshot) {
Map<String, dynamic> recipes = snapshot.data();
return SingleIngredient(ingredient_name: recipes['ingredients'].first,);
// using 'first' because ingredients is an array
},
);
Find documentation here.
body: StreamBuilder(
stream: Firestore.instance
.collection('ups')
.where('pc',
isEqualTo: widget.post.data["pc"])
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: Text("Loading..."),
);
} else {
final ups = snapshot.data.documents;
List<String> stateWidget = [];
for (var message in ups) {
final stateText = message.data['state'];
// final stateCollection = Text('$stateText');
stateWidget.add(stateText);
}enter code here
return ListView(
children: stateWidget
.map((data) => ListTile(`enter code here`
title: Text(data),
onTap: () => {_selectedItem(data)}))
.toList(),`enter code here`
)
There are two dropdown button with the list of countries and types of sport. If on them somsething is chosen it is need to show listTile with the leagues on it is chosen to the country and/or sport and if on them nothing is chosen - show all leagues.
But I get:
Dart Error: Unhandled exception:
setState () called after dispose (): _SportLigPageState # b5830 (lifecycle state: defunct, not mounted)
This is what happens if you see the widget tree (e.g.). This error can occur when a call is made. Dispose () callback. It is necessary to ensure that the object is still in the tree.
This can be a memory card if it’s not. To avoid memory leaks, consider dispose ().
Api with leagues: https://www.thesportsdb.com/api/v1/json/1/all_leagues.php:
class LigList extends StatefulWidget {
#override
_LigListState createState() => _LigListState();
}
class _LigListState extends State<LigList> {
String sport;
String country;
List data;
Future<String> getJsonData() async {
http.Response response;
if (sport != null) {
if (country != null) response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php?c=$sport&s=$country'), headers: {"Accept": "application/json"});
else response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php?c=$sport'), headers: {"Accept": "application/json"});}
else if (country == null){ response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php'), headers: {"Accept": "application/json"});}
else response = await http
.get(Uri.encodeFull('https://www.thesportsdb.com/api/v1/json/1/all_leagues.php?c=$country'), headers: {"Accept": "application/json"});
var convertDatatoJson = json.decode(response.body);
data = convertDatatoJson['leagues'];
return "Success";
}
static const menuItems = countriesList;
final List<DropdownMenuItem<String>> _dropDownItems = menuItems
.map((String CountruValue) =>
DropdownMenuItem<String>(
value: CountruValue,
child: Text(CountruValue),
),
).toList();
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Column(children: <Widget>[
FutureBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
return DropdownButton(
value: country,
hint: Text("Choose a countre league of which you want to find"),
items: _dropDownItems,
onChanged: (value) {
country = value;
print(country);
setState(() {});
},
);}),
SizedBox(width: 5),
FutureBuilder(
future: _getSports(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return snapshot.hasData
? DropdownButton(
value: sport,
hint: Text("Choose a sport league of which you want to find"),
items: snapshot.data,
onChanged: (value) {
sport = value;
print(sport);
setState(() {});
},
)
: Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: CircularProgressIndicator());
}),
Flexible(
child:FutureBuilder(
future: getJsonData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return ListView.separated(
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, int i) {
return Container(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment
.stretch,
children: <Widget>[
ListTile(
title: Text(data[i]['strLeague']),
subtitle: Text(
data[i]['strSport']),
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (
BuildContext context) =>
new ComandListScreen()
// (data[i])
));
},
),
]
)
)
);
});
}))
]),
),
);
}
}
Any assistance is very much appreciated.
There's a lot of things wrong with your code. The first child in your code is wrapped in a FutureBuilder but you're not using any Future functionality.
FutureBuilder(
builder: (BuildContext context, AsyncSnapshot snapshot) {
return DropdownButton(
value: country,
hint: Text("Choose a countre league of which you want to find"),
items: _dropDownItems,
onChanged: (value) {
country = value;
print(country);
setState(() {}); // Remove this line
},
);}),
In addition to that you also are calling setState() randomly in your onChanged callback with nothing inside of it. I'd suggest you take that widget out of the FutureBuilder and just use the DropdownButton on it's own.
Then also in this line
itemCount: data == null ? 0 : data.length,
You're using data, which is set in the future that you call there. You might want to read up on how to properly use the FutureBuilder widget. Just return the data object from your _getJsonData() Future because it's always returning "Success" anyway. Return the list you want from the Future and then access it using snapshot.data
And lastly there's literally only one setState call in there so remove it and you'll be fine. My assumption is that there's some additional dispose you're calling or navigating away and the app crashes. Will need a lot more info to figure out, but you'll have to fix the way you use Futures and the Future builder so we can ensure it's not because of latent threads coming back and setting the state once you've left the view you were on.
I am trying to fetch some data from the internet. With the use of FutureBuilder, handling for various cases like offline, online,error is quite easy but I am using StreamBuilder and I am not able to understand how to handle offline case
Following is my code to use StreamBuilder which works but I have not handled offline data or error
return StreamBuilder(
builder: (context, AsyncSnapshot<SchoolListModel> snapshot) {
if (snapshot.hasError) {
return Expanded(
child: Center(
child: Text(SOMETHING_WENT_WRONG),
));
}
if (!snapshot.hasData) {
return Expanded(
child: Center(
child: CircularProgressIndicator(),
),
);
}
if (snapshot.data != null) {
if (snapshot.data.status == 1) {
return buildSchoolList(snapshot.data.schoolListData);
} else {
showMessageDialog(snapshot.data.msg.toString(), context);
}
}
},
stream: schoolListBloc.schoolList,
);
}
Now to handle the offline case I am doing the following two options which don't work in my case
Option one.
return StreamBuilder(
builder: (context, AsyncSnapshot<SchoolListModel> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text(SOMETHING_WENT_WRONG);
case ConnectionState.active:
case ConnectionState.waiting:
return Expanded(
child: Center(
child: CircularProgressIndicator(),
),
);
case ConnectionState.done:
if (snapshot.hasError) {
return errorData(snapshot);
} else {
if (snapshot.data.status == 1) {
return buildSchoolList(snapshot.data.schoolListData);
} else {
showMessageDialog(snapshot.data.msg.toString(), context);
}
}
}
},
stream: schoolListBloc.schoolList,
);
}
I keep seeing the CircularProgressIndicator and no error on the console.
I fail to understand why the above switch case works for FuturBuilder and not StreamBuilder.
Second Option.
Future<bool> checkInternetConnection() async {
try {
final result = await InternetAddress.lookup('google.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
print('connected');
return true;
}
} on SocketException catch (_) {
print('not connected');
return false;
}
return false;
}
return StreamBuilder(
builder: (context, AsyncSnapshot<SchoolListModel> snapshot) {
checkInternetConnection().then((isAvailable) {
if (isAvailable) {
if (!snapshot.hasData || snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data != null) {
if (snapshot.data.status == 1) {
return buildSchoolList(snapshot.data.schoolListData);
} else {
showMessageDialog(snapshot.data.msg.toString(), context);
}
}
} else {
return Center(
child: Column(
children: <Widget>[
Text(CHECK_YOUR_INTERNET_CONNECTION),
RaisedButton(
onPressed: () {},
child: Text(TRY_AGAIN),
)
],
),
);
}
}); },
stream: schoolListBloc.schoolList,
);
}
Using this option throws the following error
the following assertion was thrown building StreamBuilder<SchoolListModel>(dirty, state:
I/flutter ( 5448): _StreamBuilderBaseState<SchoolListModel, AsyncSnapshot<SchoolListModel>>#dd970):
I/flutter ( 5448): A build function returned null.
I/flutter ( 5448): The offending widget is: StreamBuilder<SchoolListModel>
I/flutter ( 5448): Build functions must never return null. To return an empty space that causes the building widget to
I/flutter ( 5448): fill available room, return "new Container()". To return an empty space that takes as little room as..
What approach should I take with the following cases of offline, online and error data when using StreamBuilder
You can add Error to Stream and catch it in StreamBuilder like this:
_someStreamCtrl.addError(error); // Client is offline
And in StreamBuilder:
StreamBuilder<String>(
stream: someStream,
initialData: [],
builder: (BuildContext context,
AsyncSnapshot<String> snap) {
if (snap.hasError)
return ErrorWidget(); //Error
if (snap.hasData)
return // Desired widget
//if waiting
return CircularProgressIndicator();
);
},
);