Losing data when user terminates app then runs it again - flutter

I use firebase in almost all my app and after a doc is added I add it to a variable to structure the data and organize it but however when app is terminated then ran again the variable resets although firebase docs are added to it in the first place.
How can I fix that?
An example clarifying what I mean:
//add to firebase
final doc = <String, dynamic>{
'docType':'firebase'
};
await collection.add(doc);
//add to a model's variable
context.read<Model>().addDoc(doc['docType']);

You can listen to the stream of your snapshots, so when ever data is received you can add it to your model
//You can add that in initState for example
subscription = CloudFirestoreManager.readData.listen(
(data) async {
//Add received data to model
},
);
class CloudFirestoreManager {
static Stream<QuerySnapshot<Map<String, dynamic>>> get snapshots {
return firestoreInstance
.collection('my-id')
.snapshots();
}
static FirebaseFirestore get firestoreInstance {
return FirebaseFirestore.instance;
}
static Stream<List<Map<String, dynamic>>> get readData => snapshots.map(
(snapshot) => snapshot.docs
.map(
(doc) => doc.data(),
)
.toList(),
);
}

Related

How to merge a stream with another stream inside an await for loop - Flutter?

I have two streams coming from querying a firestore database instance like this:
Stream<Set<Place>> getUserPlaces(String userId, String listName) {
return _db
.collection('users')
.doc(userId)
.collection(listName)
.snapshots()
.map((snapshot) =>
snapshot.docs.map((doc) => Place.fromDB(doc.data())).toSet());
}
Stream<List<String>> getUserFriends(String userId) {
return _db
.collection('users')
.doc(userId)
.collection('friends')
.snapshots()
.map((snapshot) => snapshot.docs
.map(((doc) => doc.data()['friendUserId'].toString()))
.toList());
}
I have a separate Bloc class as an intermediary for my frontend UI and the backend firestore service.
class ApplicationBloc with ChangeNotifier {
final firestoreService = FirestoreService();
getAllPlace(String userId) async {
Stream<Set<Place>> userPlaces = firestoreService.getUserPlaces(userId, 'favorites');
Stream<List<String>> userFriends = firestoreService.getUserFriends(userId);
/*
Desired piece of code
*/
}
The firestoreService.getUserFriends(userId) function returns a stream of list of user ids which are strings. I want to use these returned user id strings to query my firestore db to return a set of 'Place' objects (which are a separate model not relevant to the question I think) using the firestoreService.getUserPlaces(userId, 'favorites') function.
Any ideas?
So far, I am stuck at this piece of code:
Stream<Set<Place>> userPlaces = firestoreService.getUserPlaces(userId, 'favorites');
await for (final place in userPlaces) {
/* Adding these 'place' variables to a list */
}
Stream<List<String>> userFriends = firestoreService.getUserFriends(userId);
await for (final friendUserIdList in userFriends) {
for (final friendUserId in friendUserIdList) {
Stream<Set<Place>> friendPlaces = firestoreService.getUserPlaces(friendUserId, 'favorites');
await for (final place in friendPlaces) {
/* Add these 'place' variables to the same list */
}
}
}
I am stuck because the code doesn't seem to execute the second 'await for' loop and is stuck listening to the first stream in the first 'await for' loop. I tried using the MergeStream API from RxDart to merge the first two streams but I feel the code will get stuck listening to a nested stream.

Importing an SQFlite database from Flutter app's assets and using rawQuery to display specific rows

I've built an app using Flutter. Part of its functionality is that users can search through data which is in the assets area of the app. This data was originally in JSON format, although I have converted it into an SQLite database to save storage space. That has actually helped me to save around 90%, which is great. The problem is, the search delegate no longer works. It simply returns an empty list, although no errors are produced in the console.
I have created a model class to help read the data from the SQLite database table, which looks like this:
/// Class to handle the country data in the database
class CountriesDB {
/// Defining the variables to be pulled from the json file
late int id;
late String continent;
late String continentISO;
late String country;
late String countryISO;
late String flagIconLocation;
CountriesDB({
required this.id,
required this.continent,
required this.continentISO,
required this.country,
required this.countryISO,
required this.flagIconLocation,
});
CountriesDB.fromMap(dynamic obj) {
this.id = obj[id];
this.continent = obj[continent];
this.continentISO = obj[continentISO];
this.country = obj[country];
this.countryISO = obj[countryISO];
this.flagIconLocation = obj[flagIconLocation];
}
Map<String, dynamic> toMap() {
var map = <String, dynamic>{
'id': id,
'continent': continent,
'continentISO': continentISO,
'country': country,
'countryISO': countryISO,
'flagIconLocation': flagIconLocation,
};
return map;
}
}
As far as I am aware, to read data in a database that is stored within the assets folder of the app, I need to programatically convert it into a working database. I have written the following code, to sort that:
/// Creating the database values
static final DatabaseClientData instance = DatabaseClientData._init();
static Database? _database;
DatabaseClientData._init();
/// Calling the database
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('databaseWorking.db');
return _database!;
}
/// Future function to open the database
Future<Database> _initDB(String filePath) async {
/// Getting the data from the database in 'assets'
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, filePath);
/// Check if the database exists
var exists = await databaseExists(path);
if (!exists) {
/// Should happen only the first time the application is launched
print('Creating new copy from asset');
/// Make sure the parent directory exists
try {
await Directory(dirname(path)).create(recursive: true);
} catch (_) {}
/// Copy from the asset
ByteData data =
await rootBundle.load('assets/data/database.db');
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
/// Write and flush the bytes written
await File(path).writeAsBytes(bytes, flush: true);
} else {
print('Opening existing database');
}
return await openDatabase(path, readOnly: true);
}
The next thing I have done is to create a Future function that searches the database using a rawQuery. The code for this is:
/// Functions to search for specific database entries
/// Countries
static Future<List<CountriesDB>> searchCountries(String keyword) async {
final db = await instance.database;
List<Map<String, dynamic>> allCountries = await db.rawQuery(
'SELECT * FROM availableISOCountries WHERE continent=? OR continentISO=? OR country=? OR countryISO=?',
['%keyword%']);
List<CountriesDB> countries =
allCountries.map((country) => CountriesDB.fromMap(country)).toList();
return countries;
}
Finally, I am using the Flutter Search Delegate class to allow the user to interact with the database and search for specific rows. This is the widget I have built for that:
/// Checks to see if suggestions can be made and returns error if not
Widget buildSuggestions(BuildContext context) => Container(
color: Color(0xFFF7F7F7),
child: FutureBuilder<List<CountriesDB>>(
future: DatabaseClientData.searchCountries(query),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(
child: PlatformCircularProgressIndicator(
material: (_, __) => MaterialProgressIndicatorData(
color: Color(0xFF287AD3),
),
cupertino: (_, __) => CupertinoProgressIndicatorData(),
));
default:
if (query.isEmpty) {
return buildAllSuggestionsNoSearch(snapshot.data!);
} else if (snapshot.hasError || snapshot.data!.isEmpty) {
return buildNoSuggestionsError(context);
} else {
return buildSuggestionsSuccess(snapshot.data!);
}
}
},
),
);
The idea is that the functionality I have built will return the whole list before a user searches and once a users starts typing, they will only be shown any rows that match their search query. This worked fine when I was using JSON data but it is returning an empty list, yet there are no errors printed in the console, at all. That makes it quite hard to know where my code is going wrong.
Where have I gone wrong with my code, such that it is not returning any data? How can I correct this? Thanks!

transform from Firestore API to firebase rtdb api

recently im learning to create a event from a tutorial, but the original one is made in firestore, and Im trying to use firebase rtdb,
here is original code:
class FirebaseApi {
static Future<String> createTodo(Todo todo) async {
final docTodo = FirebaseFirestore.instance.collection('todo').doc();
todo.id = docTodo.id;
await docTodo.set(todo.toJson());
return docTodo.id;
}
}
here is code I created, sorry my basic knowledge is now good, Idk what should i return
class FirebaseApi {
static Future<String> createTodo(Todo todo) async{
final todoRefMessages = FirebaseDatabase.instance.ref().child('todo');
final newTodo = FirebaseDatabase.instance.ref().child('todo').get().then((snapshot) async{
final json = Map<dynamic, dynamic>.from(snapshot.value);
final newTodo = Todo(
createdTime: Utils.toDateTime(json['createdTime']),
title: json['title'],
description: json['description'],
id: json['id'],
isDone: json['isDone'],
);
await todoRefMessages.set(newTodo.toJson());
return newTodo;
});
todo.id= newTodo.id;//here got error, The getter 'id' isn't defined for the type 'Future<Todo>
return newTodo.id;
}
}
could you please let me know how to create same method but for firebase rtdb, thanks in advance!
This call to Firestore give you a reference to a new non-existing document in the todo collection:
final docTodo = FirebaseFirestore.instance.collection('todo').doc();
Is equivalent to this in the Realtime Database is:
final todoRef = FirebaseDatabase.instance.ref("todo").push();
Then to store the JSON to that document in Firestore you do:
await docTodo.set(todo.toJson());
The equivalent in the Realtime Database is:
await todoRef.set(todo.toJson());
If you have errors in other parts of your code, I recommend keeping the functionality of your createTodo method exactly the same between the Firestore and Realtime Database implementations.
For example, I assume that the Todo todo object is exactly the same between the implementations. If it isn't, the problem is less likely to be in the database API calls, but probably in the implementation of Todo.

Get value from one future and use it in another future, with Flutter

I have a favourites collection saved under a users collection. Each of the favourite documents has one field which contains a product_Id. I want to retrieve this product_id value and use it to query another collection. This second collection holds the actual products documents.
Retrieving all the documents in the favourite collection. What do I do next to get the value of the product_id fields as strings?
getIdsfromUserFavs(userId) async {
var _favData = await _usersCollectionReference
.doc(userId)
.collection('favourites')
.get();
}
This is the second method that is used to query the products collection. This method needs the String value from above in order to successfully make the query.
Future<QuerySnapshot<Object?>> queryFavsCollection(value) async {
var _favedProducts = await _productsCollectionReference
.where('prod_id', isEqualTo: value)
.get();
print(value);
return _favedProducts;
}
I am using a futureBuilder in the UI.
THis is one way I have tried(The problem with this is that I don't get any data returned):
getIdsfromUserFavs(userId) async {
var _favData = await _usersCollectionReference
.doc(userId)
.collection('favourites')
.get();
var allData = _favData.docs.map((doc) => doc.data()).toList();
allData.forEach((element) async {
String value = element['prod_id'];
print(value);
await queryFavsCollection(value);
});
}
Future<QuerySnapshot<Object?>> queryFavsCollection(value) async {
var _favedProducts = await _productsCollectionReference
.where('prod_id', isEqualTo: value)
.get();
print(value);
return _favedProducts;
}
I can see that the above methods print the ids to the console. But the FutureBuilder doesn't receive any data:
I/flutter ( 4628): 3nHHEWuCDXvbhYfT8ljY
I/flutter ( 4628): MptYFV1oXhflDYkdQyIP
I/flutter ( 4628): Fd2ntXyNVmjn0D6mG3RA
Below function will return all data from favourite collection
Future<QuerySnapshot<Map<String, dynamic>>> getIdsfromUserFavs(userId) async {
QuerySnapshot<Map<String, dynamic>> _favData = await _usersCollectionReference
.doc(userId)
.collection('favourites')
.get();
return _favData; // This will return all data of favourite collection
}
After that you can return List of desire data as shown in below function
Future<List<QueryDocumentSnapshot<Map<String, dynamic>>>> queryFavsCollection(userId) async {
// Will store data in this list so at the end we can return this
List<QueryDocumentSnapshot<Map<String, dynamic>>> favData = [];
QuerySnapshot<Map<String, dynamic>> _favData =
await getIdsfromUserFavs(userId);
for (QueryDocumentSnapshot<Map<String, dynamic>> data in _favData.docs) {
String value = data['prod_id'];
QuerySnapshot<Map<String, dynamic>> _fav = await
_productsCollectionReference
.where('prod_id', isEqualTo: value)
.get();
if (_fav.docs.isNotEmpty) {
_fav.docs.forEach((element) {
favData.add(element);
});
}
}
return favData;
}
Now you can use FutureBuilder as shown below
FutureBuilder<List<QueryDocumentSnapshot<Map<String, dynamic>>>>(
future: queryFavsCollection(userId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('Loading...');
}
return Text('you data');
},
);
For better practice kindly refer this. This is from flutter documentation
"The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted."

How can I use a stream with a regular provider, updating the local values as the stream is delivered

So I have something like this which works fine with streamprovider.
Stream<List<AppUser>> streamUsers() {
return firestore.collection('users').snapshots().map(
(snapshot) => snapshot.docs
.map((document) => AppUser.fromJson(document.data()))
.toList(),
);
}
What I'm looking to achieve is something like this, where the stream will run in the background, and everytime data is transmitted it updates a local variable in my provider and notifies my widgets through provider that way. Is it bad idea? If so, why?
class AppUsers with ChangeNotifier {
FirebaseFirestore firestore = FirebaseFirestore.instance;
List<AppUser> _users;
List<AppUser> get users => _users;
void streamUsers() {
List<AppUser> users = [];
firestore
.collection('users')
.snapshots()
.map((snapshot) => snapshot.docs.map((document) {
AppUser user = AppUser.fromJson(document.data());
users.add(user);
}));
_users = users;
notifyListeners();
}
}
UPDATE
I was able to achieve this with the following code and calling init() when my app loads. I want to avoid calling init when my app loads tho. Is there a cleaner way?
List<AppUser> _users = [];
List<AppUser> get users => _users;
init() {
streamUsers().listen((event) {
_users = event;
notifyListeners();
});
}
Stream<List<AppUser>> streamUsers() {
return firestore.collection('users').snapshots().map(
(snapshot) => snapshot.docs
.map((document) => AppUser.fromJson(document.data()))
.toList(),
);
}
Remove the init function and use notify listener every stream event by mapping once more :
Stream<List<AppUser>> streamUsers() {
return firestore.collection('users').snapshots().map(
(snapshot) => snapshot.docs
.map((document) => AppUser.fromJson(document.data()))
.toList(),
).map((users) {
_users = users;
notifyListeners();
});
}