How to cache Firebase data in Flutter? - firebase

In my app I build a list of objects using data from Firebase. Inside a StreamBuilder, I check if the snapshot has data. If it doesen't, I am returning a simple Text widget with "Loading...". My problem is that if I go to another page in the app, and then come back, you can see for a split second that it says 'Loading...' in the middle of the screen, and it is a bit irritating. I am pretty sure it is downloading the data from Firebase, and building the widget every time I come back to that page. And if I don't do the check for data, it gives me a data that I am trying to access data from null.
Is there a way to cache the data that was already downloaded, and if there has been no change in the data from Firebase, then just use the cached data?
Heres a redacted version of my code:
class Schedule extends StatefulWidget implements AppPage {
final Color color = Colors.green;
#override
_ScheduleState createState() => _ScheduleState();
}
class _ScheduleState extends State<Schedule> {
List<Event> events;
List<Event> dayEvents;
int currentDay;
Widget itemBuilder(BuildContext context, int index) {
// Some Code
}
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
events = new List(snapshot.data.documents.length);
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
events[i] = Event(
name: doc["name"],
start: DateTime(
doc["startTime"].year,
doc["startTime"].month,
doc["startTime"].day,
doc["startTime"].hour,
doc["startTime"].minute,
),
end: DateTime(
doc["endTime"].year,
doc["endTime"].month,
doc["endTime"].day,
doc["endTime"].hour,
doc["endTime"].minute,
),
buildingDoc: doc["location"],
type: doc["type"],
);
}
events.sort((a, b) => a.start.compareTo(b.start));
dayEvents = events.where((Event e) {
return e.start.day == currentDay;
}).toList();
return ListView.builder(
itemBuilder: itemBuilder,
itemCount: dayEvents.length,
);
},
),
);
}
}

You can use the the following code to define the source you want to retrieve data from. This will search either in local cache or on the server, not both. It works for all get() parameters, no matter if it is a search or document retrieval.
import 'package:cloud_firestore/cloud_firestore.dart';
FirebaseFirestore.instance.collection("collection").doc("doc").get(GetOptions(source: Source.cache))
To check if the search has data in cache, you need to first run the search against cache and if there is no result, run it against the server.
I found project firestore_collection to use a neat extension that can greatly simplify this process.
import 'package:cloud_firestore/cloud_firestore.dart';
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_document.dart
extension FirestoreDocumentExtension on DocumentReference {
Future<DocumentSnapshot> getSavy() async {
try {
DocumentSnapshot ds = await this.get(GetOptions(source: Source.cache));
if (ds == null) return this.get(GetOptions(source: Source.server));
return ds;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
}
// https://github.com/furkansarihan/firestore_collection/blob/master/lib/firestore_query.dart
extension FirestoreQueryExtension on Query {
Future<QuerySnapshot> getSavy() async {
try {
QuerySnapshot qs = await this.get(GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return this.get(GetOptions(source: Source.server));
return qs;
} catch (_) {
return this.get(GetOptions(source: Source.server));
}
}
If you add this code, you can simply change the .get() command for both documents and queries to .getSavy() and it will automatically try the cache first and only contact the server if no data can be locally found.
FirebaseFirestore.instance.collection("collection").doc("doc").getSavy();

To be sure whether the data is coming from Firestore's local cache or from the network, you can do this:
for (int i = 0; i < snapshot.data.documents.length; i++) {
DocumentSnapshot doc = snapshot.data.documents.elementAt(i);
print(doc.metadata.isFromCache ? "NOT FROM NETWORK" : "FROM NETWORK");
In the case you described you are probably going to still see the loading screen when its "NOT FROM NETWORK". This is because it does take some time to get it from the local cache. Soon you will be able to ask for the query's metadata for cases with empty results.
Like others suggested, you can cache the results and you won't see this. First you can try to cache it in the Widget using something like:
QuerySnapshot cache; //**
#override
Widget build(BuildContext context) {
return Center(
child: StreamBuilder(
initialData: cache, //**
stream: Firestore.instance.collection('events').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text("Loading...");
}
cache = snapshot.data; //**
This will make your widget remember the data. However, if this does not solve your problem, you would have to save it not in this widget but somewhere else. One option is to use the Provider widget to store it in a variable that lives beyond the scope of this particular widget.
Probably not related, but it's also a good idea to move the Firestore.instance.collection('events').snapshots() to initState(), save the reference to the stream in a private field and use that it StreamBuilder. Otherwise, at every build() you may be creating a new stream. You should be ready for build() calls that happen many times per second, whatever the reason.

Using Generics
Appending to #James Cameron's answer above; I found myself in a situation where, said implementation removed my typecast from withConverter. So, the below adds the generic types back into the functions.
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
extension FirestoreDocumentExtension<T> on DocumentReference<T> {
Future<DocumentSnapshot<T>> getCacheFirst() async {
try {
var ds = await get(const GetOptions(source: Source.cache));
if (!ds.exists) return get(const GetOptions(source: Source.server));
return ds;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
extension FirestoreQueryExtension<T> on Query<T> {
Future<QuerySnapshot<T>> getCacheFirst() async {
try {
var qs = await get(const GetOptions(source: Source.cache));
if (qs.docs.isEmpty) return get(const GetOptions(source: Source.server));
return qs;
} catch (_) {
return get(const GetOptions(source: Source.server));
}
}
}
use_case.dart
The implementation below would not compile with James example, as DocumentSnapshot<Object?> is not a subset of DocumentSnapshot<UserModel>. So, by adding the generic parameters back in, we can ensure that this extension maintains any type casts.
Future<DocumentSnapshot<UserModel>> userInfo() async {
return await FirebaseFirestore.instance
.doc("${path_to_user_model_doc}")
.withConverter<UserModel>(
fromFirestore: (snapshot, _) => UserModel.fromJson(snapshot.data()!),
toFirestore: (userModel, _) => userModel.toJson(),
)
.getCacheFirst();
}
pubspec.yaml
environment:
sdk: ">=2.17.1 <3.0.2"
dependencies:
cloud_firestore: ^3.1.17

Related

Flutter subscribe/query to one field in one file in cloud firestore

New to booth flutter and stackoverflow.
I am making the account verification functionally for my flutter app. My plan is to divided this functionally into two parts, part one shows an alertdialog when the screen is built, and part two checks if the "activated" field in firestore is true or false. I have problem of making part two.
This is what I write for part one
String uid = "fdv89gu3njgnhJGBh";
bool isActivated = false;
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if (isActivated == false) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: AlertDialog(
title: Text("Activation pending"),
content: Text("Your account is waiting to be activate by admin"),
actions: [
FlatButton(
child: Text("Refresh"),
onPressed: () {
// just bring reassurance to user
},
),
],
),
);
});
}
});
}
For part two I plan to make a Future return type function, what it will do is to subscribe the boolean value that stored in firestore: /user/uid/activated, once the function gets a "true" from firestore, it will return it to part one and part one will close the alertdialog(which I haven't figure out how to do this).
I've already seen some solutions from the internet but most solutions involve StreamBuilder, but it seems that I don't need to build any widget for the stream in part two. Is it better to just make changes to what I write previously* or integrate both parts two one StreamBuilder function?
*What I wrote for get the data from one field among all files (and this works well):
Future<bool> registeredCheck(String email) async {
var userInfo = await _firestore.collection("user").get();
for (var userInf in userInfo.docs) {
if (userInf.data()["email"] == email) {
return true;
}
}
return false;
}
}
Thank you
You don't have to query the entire collection. Since you already know the uid, you can just get the document of the uid directly like this:
Future<bool> registeredCheck(String email) async {
final userDoc = await _firestore.collection("user").doc(uid).get();
return userDoc.data()['activated'] ?? false;
}
The reason why I am adding ?? false is to return false instead of null when the activated value is null;

Adding up values stored in Firebase and displaying total in a FutureBuilder

Im trying to retrieve a list of monthly expenses from Firebase, add up the amount of each monthly expense and show it in a FutureBuilder.
In the Text widget i'm simply getting null. Been trying to google an answer for the past half hour but i don't even know where to begin as in what to search for.
Tried throwing in some print statement to see whats going on but any print statement I put in after the provider call doesn't show.
EDIT: Added Future casts to the commented lines. still no luck. but ignored the variable and called the future function directly from the FutureBuilder. Now all print statements are working and it is returning an instance of 'MonthlyExpense'. It is null still.
class SummaryView extends StatefulWidget {
#override
_SummaryViewState createState() => _SummaryViewState();
}
class _SummaryViewState extends State<SummaryView> {
/* Removed future variable and initState()
Future<double> _totalExpensesAmount; //Future<double> added
#override
void initState() {
super.initState();
print("init");
_totalExpensesAmount = _getTotalExpensesAmount();
}
*/
Future<double> _getTotalExpensesAmount() async { //Future<double> added
print("started");
final user = Provider.of<BPUser>(context);
print("user: $user");
double expensesTotal = 0.0;
var snapshot = await FirebaseFirestore.instance
.collection("BPUsers/${user.uid}/monthlyexpenses")
.get();
print(snapshot);
List<MonthlyExpense> searchedProducts =
DatabaseService().monthlyExpenseListFromSnapshot(snapshot);
print(searchedProducts);
for (var i = searchedProducts.length; i >= 1; i--) {
expensesTotal += double.parse(searchedProducts[i].amount);
}
return Future<double>expensesTotal; //Future<double> added
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: mainBackgroundColor,
body: Column(
children: [
Container(
child: FutureBuilder(
future: _getTotalExpensesAmount(), // called the function directly
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(
"${snapshot.data}",
style: nameStyle,
);
} else {
return Text("Loading...");
}
}),
)
],
),
);
}
}
The monthlyExpenseListFromSnapshot function (which works perfectly in another widget I use it in):
List<MonthlyExpense> monthlyExpenseListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map((doc) {
return MonthlyExpense(
name: doc.data()['name'] ?? '',
amount: doc.data()['amount'] ?? '',
isActive: doc.data()['isActive']);
}).toList();
}
The firebase database:
You're getExpensesTotal doesn't return a future. Because of this, the widgetvwill never rebuild since the data is already calculated. FutureBuilder and StreamBuilder only cause a rebuild AFTER data been loaded after the initial build. Try surrounding your expensesTotal with Future.value.
You need to define the signature of the function to represent what it is expected, in this case Future <double>.
// ....
Future<double> _getTotalExpensesAmount() async {
First issue was to skip the variable and the init call and call the _getTotalExpensesAmount() future function (adding the Future signature to it).
Secondly my for loop was wrong. needed to change the conditions to:
for (var i = searchedProducts.length - 1; i >= 0; i--) {
Everything is working fine now!

Returning null user data from Firestore. How to reference it globaly instead?

I'm quite new to Flutter and I've been struggling to access a user's document on Firestore.
On the profile page,
I'm setting the current user's UID inside initState, but uid returns null for a quick second, then the page updates with correct info.
So I am able to retrieve a certain field (like displayName), but it isn't quite the best practice. I don't want to have a bunch of boilerplate code and await functions mixed with UI and such.
Code:
FirebaseUser user;
String error;
void setUser(FirebaseUser user) {
setState(() {
this.user = user;
this.error = null;
});
}
void setError(e) {
setState(() {
this.user = null;
this.error = e.toString();
});
}
#override
void initState() {
super.initState();
FirebaseAuth.instance.currentUser().then(setUser).catchError(setError);
}
Then in my body I have a Stream builder to get the document.
body: StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(user.uid)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
);
} else {
var userDocument = snapshot.data;
return showProfileHeader(userDocument);
}
},
)
I want to make 'global' references to be accessed throughout the app. Instead of getting the user's id on every page and streaming a specific field when I might need multiple ones.
The only ways I found online to do something similar, created lists with all the data in it. I feel like this might get extra fields I don't need.
How can I make data from Firestore available across the app?
I am using the "Provider" package for doing state management across my app. Nowadays its also the suggested way by the google flutter team when it comes to state management. See the package here: https://pub.dev/packages/provider
Regarding Firebase Auth and accessing the credentials application wide, i am using that said package like stated on this page:
https://fireship.io/lessons/advanced-flutter-firebase/
Short version below. Bootstrap your app like so:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Make user stream available
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged),
// not needed for your problem but here you can see how
// to define other Providers (types) for your app.
// You need a counter class which holds your model of course.
ChangeNotifierProvider(builder: (_) => Counter(0)),
],
// All data will be available in this child and descendents
child: MaterialApp(...)
);
}
}
Then in your child widgets, just do:
// Some widget deeply nested in the widget tree...
class SomeWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<FirebaseUser>(context);
return Text(user.displayName) // or user.uid or user.email....
}
}
This should do the trick.
That happens because FirebaseAuth.instance.currentUser() returns a future, and until that future is completed, you will not have the proper FirebaseUser object.
Making the user object global is not a bad idea. In addition, you can hook it up to the FirebaseAuth stream so that it gets updated everytime the user auth status changes, like so in a user.dart file:
class User {
static FirebaseUser _user;
static get user => _user;
static void init() async {
_user = await FirebaseAuth.instance.currentUser();
FirebaseAuth.instance.onAuthStateChanged.listen((firebaseUser) {
_user = firebaseUser;
});
}
}
You can call User.init() in main() and access the user object with User.user.

Flutter can't read from Clipboard

I come asking for quite a specific question regarding Flutter and the Future and await mechanism, which seems to be working, but my Clipboard does not really function while operating with my editable text fields, even following Google's advice on implementation...
This is my code for pasting:
onPressed: () async {
await getMyData('text');
_encodingController.text = clipData;
Scaffold.of(context).showSnackBar(
new SnackBar(
content: new Text(
"Pasted from Clipboard"),
),
);
},
what doesnt work is my paste functionality... While debugging the result of this following function is null, wth?????????
static Future<ClipboardData> getMyData(String format) async {
final Map<String, dynamic> result =
await SystemChannels.platform.invokeMethod(
'Clipboard.getData',
format,
);
if (result == null) {
return null;
} else {
clipData = ClipboardData(text: result['text']).text;
return ClipboardData(text: result['text'].text);
}
}
I am probably using the Futures and async await wrong, would love some guidance!!! Copying is working using the Clipboard Manager plugin! Thanks very much!
You can simply re-use Flutter's existing library code to getData from Clipboard.
ClipboardData data = await Clipboard.getData('text/plain');
First create a method
Future<String> getClipBoardData() async {
ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
return data.text;
}
Then in build method
FutureBuilder(
future: getClipBoardData(),
initialData: 'nothing',
builder: (context, snapShot){
return Text(snapShot.data.toString());
},
),
It's works for me:
_getFromClipboard() async {
Map<String, dynamic> result =
await SystemChannels.platform.invokeMethod('Clipboard.getData');
if (result != null) {
return result['text'].toString();
}
return '';
}
Also can be useful if you want to listen for periodic updates from the system clipboard.
Originally I replied here, just re-posting the solution:
#creating a listening Stream:
final clipboardContentStream = StreamController<String>.broadcast();
#creating a timer for updates:
Timer clipboardTriggerTime;
clipboardTriggerTime = Timer.periodic(
# you can specify any duration you want, roughly every 20 read from the system
const Duration(seconds: 5),
(timer) {
Clipboard.getData('text/plain').then((clipboarContent) {
print('Clipboard content ${clipboarContent.text}');
# post to a Stream you're subscribed to
clipboardContentStream.add(clipboarContent.text);
});
},
);
# subscribe your view with
Stream get clipboardText => clipboardController.stream
# and don't forget to clean up on your widget
#override
void dispose() {
clipboardContentStream.close();
clipboardTriggerTime.cancel();
}

How to finish the async Future task before executing the next instruction Flutter

I make a function call to my database, which updates a local object after getting data and takes a few moments.
Because of the Async task, the program moves to the next line of code. unfortunately I need the local object that gets updated with the async call for the next line of code.
how can I wait for my async task to finish before the next piece of code is executed? thank you
edit: adding code to explain
updateUser() {
return FutureBuilder(
future: updateUserData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Text("hello not");
} else {
return Text('Hello!');
}
},
);}
#override
Widget build(BuildContext context) {
switch (_authStatus) {
case AuthStatus.notSignedIn:
return new LoginPage(
auth: auth,
CurrentUser: CurrentUser,
onSignedIn: _signedIn,
);
case AuthStatus.signedIn:
{
updateUser(); //THIS TAKES A COUPLE SECONDS TO FINISH BUT I NEED TO SEND IT TO THE NEXT PAGE
return new HomePage(
auth: auth,
CurrentUser: CurrentUser,
onSignedOut: _signedOut,
);
}
}
}
}
You can use await keyword in async function.
eg:
void someFunc() async {
await someFutureFunction();
// Your block of code
}
Here your block of code wont run until someFutureFunction returns something.
You can also use with custom async function like below example:
(() async {
await restApis.getSearchedProducts(widget.sub_cats_id,widget.keyword).then((val) => setState(()
{
setState(() {
data = val["data"];
});
}));
})();
This might help, The below sample code has two functions,
Below function is used to load the assets and return the JSON content.
Future<String> _loadCountriesAsset() async {
return await rootBundle.loadString('assets/country_codes.json');
}
the other function will use the JSON content and convert the format to model and return to the class.
Future<List<CountryCode>> loadCountryCodes() async {
String jsonString = await _loadCountriesAsset();
final jsonResponse = json.decode(jsonString);
// print(jsonResponse);
CountriesCodeList countriesCodeList =
new CountriesCodeList.fromJson(jsonResponse);
// print("length " + countriesCodeList.codes.length.toString());
return countriesCodeList.codes;
}
Usage in the class, defined a method to call the loadCountryCodes() function from services.dart file.
Future getCountryCodes() async {
var countryCodesList = await loadCountryCodes();
print("**** length " + countryCodesList.length.toString());
// Perform the operations, or update to the UI or server. It should be same for API call as well.
}
Hope this helps.

Resources