Using RxList variable and trigger other RxList variable on GetxController streamBinding() with firebase - firebase

I am using Firebase Firestore. and also
GetxController, RxList, Streaming
First of all, I would like to describe my scenario roughly.
In my scenario I am adding or removing the Clubs array in the player collection. This change I made is correctly reflected in the AccountController.clubs array variable. but the list from the readClubs method, where I pulled the details of the club information, is not updated. I am using the AccountController.clubs array list in the where condition in the readClubs() method. Although the array list is updated, the _clubs RxList variable and ClubController.clubs object list, where I keep the detailed club list, are not updated. I can see the correct values only when I restart the application
I have shared the relevant branches below. I haven't shared the view codes because I can't see the correct values in the variables yet.
Data Model
//Players:
[
{
"playerUid":"1",
"Name":"Ramazan",
"Clubs":["1","2"]
}
]
//Clubs:
[
{
"clubUid":"1",
"Name":"Club1"
},
{
"clubUid":"2",
"Name":"Club2",
},
{
"clubUid":"3",
"Name":"Club3"
}
]
AccountController
onInit() async {
super.onInit();
_loggedAccount.bindStream(Database.initAccount(uid));
}
final Rx<AccountModel> _loggedAccount = AccountModel().obs;
List<String> get clubs => _getPlayerCLubsUids();
AccountModel get loggedAccount => _loggedAccount.value;
List<String> _getPlayerCLubsUids() {
List<String> clubUids = [];
if (loggedAccount.clubs != null) {
for (var e in loggedAccount.clubs!) {
clubUids.add(e.uid);
}
}
return clubUids;
}
ClubController
final RxList<ClubModel> _clubs = <ClubModel>[].obs;
List<ClubModel> get clubs => _getCLubs();
#override
void onInit() {
super.onInit();
_clubs.bindStream(Database.readClubs());
}
Database
static Stream<List<ClubModel>> readClubs() {
if (AccountController.to.clubs.isNotEmpty) {
return _firestore
.collection('clubs')
.where('uid', whereIn: AccountController.to.clubs)
.snapshots()
.map((QuerySnapshot query) {
List<ClubModel> retVal = [];
for (var element in query.docs) {
retVal.add(ClubModel.fromSnapshot(element));
}
return retVal;
});
} else {
return const Stream.empty();
}
}
The part I think is causing the problem. in Database.read Clubs() because I have same matzot without where condition.
.where('uid', whereIn: AccountController.to.clubs)

Related

2 updates within one Firestore transaction

I am new to Firestore transaction, and would like to update a document field based the current data of the document.
My planned transaction is given below:
const cityRef = db.collection('cities').doc('SF');
try {
await db.runTransaction(async (t) => {
const doc = await t.get(cityRef);
let status = doc.data().myStatus;
if (status == "one") {
throw "err";
} else {
// run some function - next status is based on the return
let output = await someFunction();
if (output) {
await t.update(cityRef, { myStatus: "two" });
return output;
} else {
await t.update(cityRef, { myStatus: "three" });
return output;
}
}
});
console.log("transaction successful");
} catch (err) {
console.log("Alreadu updated");
output = "one";
return output;
}
My queries are given below:
As per the documentation I have returned the data after update, however it does not seem to be working as expected.
Can we have 2 updates within one single transaction (both are updating the same field in the firestore)?
Thank you
You make the following clarification in the comments above:
someFunction() does some processing on other firestore
collection/documents (not the one I am updating) and returns either
true or false.
As you read in the doc on Transactions, "Read operations must come before write operations". If you want to read some docs in the transaction, you need to use the get() method of the Transaction, like you did with the first document. You cannot call a function that is using other Firestore methods like the get() method of a DocumentReference.

Flutter: How to loop through each field in `DocumentSnapshot.data()`

I have a collection where each user has their own document. In the document I create a map for a new entry. The document would look something like this:
In my app, I want to get the document and then turn it into a list where each map (titled by the time) would be an entry. I tried looping through it but the problem is that there is not a forEach() in the DocumentSnapshot.data() and it is a type of _JsonDocumentSnapshot.
How can I get this as a List where 2021-05-30 20:49:59.671705, 2021-05-30 20:50:00:600294, ... are individual elements in a list. I plan on building each entry on its own using something such as a ListView.builder() so I would like to have a list of all those entries.
Edit:
My code looks something like this:
journal is just a DocumentReference.
journal.snapshots().forEach((element) {
var data = element.data();
print(element.data());
(data ?? {}).forEach((key, value) {
return JournalEntryData(
date: key,
entryText: value['entryText'],
feeling: value['feeling']);
});
});
The problem was that the type of data which was set to element.data() was Object? hence the forEach() method wasn't defined. I tried casting the element.data() as a List which caused errors so I didn't go further down that path. What I should have done was to cast element.data() as a Map which worked. I also should have used journal.get() and not snapshots() as it is a stream so the await keyword would result in the journal.snapshots().forEach() to never end.
The final code looked something like this:
Future<List<JournalEntryData>> getJournalEntries() async {
List<JournalEntryData> entries = [];
await journal.get().then((document) {
Map data = (document.data() as Map);
data.forEach((key, value) {
print('adding entry');
entries.add(JournalEntryData(
date: key,
entryText: value['entryText'],
feeling: value['feeling'],
));
});
});
return entries;
}
and I would do the following to get the entries:
var entries = await getJournalEntries();
Hey for most of my problems like these I choose to go with this approach;
final result = await _db
.collection("collectionName")
.get();
List<Object> toReturn = [];
for (int i = 0; i < result.docs.length; i++) {
// add data to list you want to return.
toReturn.add();
}
Hopefully, this will be helpful to you.
List collectionElements = [];
void messagesStream() async {
await for (var snapshot in _firestore.collection('your collection').snapshots().where(
(snapshot) => snapshot.docs
.every((element) => element.author == currentUser.id),
)) {
collectionElements.add(snapshot);
}
print(collectionElements);
}
Each element would be added to the collectionElements list.
If you want to access only one field in your snapshot this works fine:
final entryText= element.data()['entryText'];
print(entryText); // this would print "lorem ipsum..."

Why Firebase Task in RxJava Completable emitter doesn't execute?

I'm developing a Firebase Android application which connect to a Firestore. The nomenclature is that the collection is "Assets". The example code had simple actions like addAsset and deleteAsset, those work fine. This is the data repository layer which actually converse with Firebase, the view model layer is above this.
class FirestoreAssetRepository(secondaryDB: FirebaseFirestore) : IAssetRepository {
companion object {
private const val TAG = "FirestoreAssetRepo"
private const val ASSET_COLLECTION = "Assets"
}
private var remoteDB: FirebaseFirestore
private var changeObservable: Observable<List<DocumentSnapshot>>
init {
remoteDB = secondaryDB
}
override fun addAsset(asset: Asset): Completable {
return Completable.create { emitter ->
remoteDB.collection(ASSET_COLLECTION)
.add(mapToAssetData(asset))
.addOnSuccessListener {
if (!emitter.isDisposed) {
emitter.onComplete()
}
}
.addOnFailureListener {
if (!emitter.isDisposed) {
emitter.onError(it)
}
}
}
}
override fun deleteAsset(assetId: String): Completable {
return Completable.create { emitter ->
remoteDB.collection(ASSET_COLLECTION)
.document(assetId)
.delete()
.addOnSuccessListener {
if (!emitter.isDisposed) {
emitter.onComplete()
}
}
.addOnFailureListener {
if (!emitter.isDisposed) {
emitter.onError(it)
}
}
}
}
I'm adding an action to the repository which would modify a specific document.
override fun lockUnlockAsset(assetId: String): Completable {
Log.d(TAG, "lockUnlockAsset")
return Completable.create { emitter ->
remoteDB.collection(ASSET_COLLECTION)
.document(assetId)
.get()
.addOnSuccessListener {
Log.d(TAG, "Unlocking")
val remoteAsset = mapDocumentToRemoteAsset(it)
it.reference.update(getUnlockLocation())
if (!emitter.isDisposed) {
emitter.onComplete()
}
}
.addOnFailureListener {
Log.d(TAG, "Could not find asset to unlock")
if (!emitter.isDisposed) {
emitter.onError(it)
}
}
}
}
The execution reaches Log.d(TAG, "lockUnlockAsset") but never gets to Log.d(TAG, "Unlocking"). If I place a break point at that second logging command it is the usual red dot in the beginning, but when the call comes into the function the icon changes to a grey "don't enter" icon and when I hover over it Android Studio tells me that "No executable found at ...". So something is definitely wrong there.
I'm new to Kotlin and RxJava2. How can I get this to work?
Update: to answer Pavel's question: these functions are called from the ViewModel layer:
fun deleteAsset(assetId: String) {
repository.deleteAsset(assetId)
.subscribeOn(Schedulers.io())
.subscribe(
{},
{
it.printStackTrace()
})
.addTo(disposable)
}
fun addAsset(assetTitle: String) {
repository.addAsset(Asset("${System.currentTimeMillis()}", assetTitle))
.subscribeOn(Schedulers.io())
.subscribe(
{},
{
it.printStackTrace()
})
.addTo(disposable)
}
fun lockUnlockAsset(assetId: String) {
repository.lockUnlockAsset(assetId)
}
I was experimenting with combinations of .subscribeOn(Schedulers.io()).observe at the repository level. Maybe it's the .addTo(disposable) which got it working, I'm not sure what I was missing. Now it's working, I wait for Pavel for his answer.
I experimented with combinations of .subscribeOn(...) and observeOn(..) + .observe(...) at the data repository level, but I should have just followed the pattern in the view model (view model calls the functions of the data repository): it's a chained subscribeOn + subscribe + addTo(disposable):
fun lockUnlockAsset(assetId: String) {
repository.lockUnlockAsset(assetId)
.subscribeOn(Schedulers.io())
.subscribe(
{},
{
it.printStackTrace()
})
.addTo(disposable)
}
Thanks for Pavel for pointing this out.

How to filter list of id on firebase cloud firestore?

I have two collections. (applyJobs and Jobs and users). When users apply for a job, I store that record inside applyJobs collection. Like this:
applyId:****,
jobId:*****,
userId:*****
Now, I want to show all apply for jobs by a user.
First: Get logged user id, I store locally logged user id. So, I can get loggged user id.
Second: I filter Apply Jobs by that id. like this, var ref = _db.collection('applyJobs').where('userId',isEqualTo: uid);. I here I didn't call users collection to get uid. because I already store uid on locally. Is it best practice?
Third: I store result here List<ApplyJobsModelClass>. I want to get all jobs by a list of id. How do I filter it?
This is way I tried it. But this is not list of IDs. only one id.
streamApplyJob(List<String> jobId) {
Collection('jobs').document(jobId);
}
And I tried this way too.
Stream<List<JobModel>> streamApplyJob(List<String> jobId) {
var ref = _db.collection('jobs').where('jobId',isEqualTo: jobId);
return ref.snapshots().map((list) =>
list.documents.map((doc) => JobModel.fromFirestore(doc)).toList());
}
tried to get length, but result is 0
db.streamApplyJob(jobIds).listen((v)=>{
print(v.length)
});
Full Code
Database side
///Get a stream of apply jobs
Stream<List<ApplyJobModel>> streamApplyJobs(String uid) {
var ref = _db.collection('applyJobs').where('userId',isEqualTo: uid);
return ref.snapshots().map((list) =>
list.documents.map((doc) => ApplyJobModel.fromFirestore(doc)).toList());
}
///Get a stream of a single document
Stream<List<JobModel>> streamApplyJob(List<String> jobId) {
var ref = _db.collection('jobs').where('jobId',isEqualTo: jobId);
return ref.snapshots().map((list) =>
list.documents.map((doc) => JobModel.fromFirestore(doc)).toList());
}
calling
List<String> jobIds = [];
void getData() {
db.streamApplyJobs(widget.uid).listen((listApplies) => {
for (int i = 0; i < listApplies.length; i++)
{jobIds.add(listApplies[i].jobId)},
});
db.streamApplyJob(jobIds).listen((v)=>{
print(v.length)
});
}
Solution(It's working now)- Is it best practice or are there other best way to do this?
Future<List<JobModel>> getJobs() async {
await db.streamJobs(true).listen((jobs) {
setState(() {
jobModel = jobs;
});
});
return jobModel;
}
Future getData() async {
await getJobs();
db.streamApplyJobs(widget.uid).listen((apply) => {
for (int j = 0; j < jobModel.length; j++)
{
for (int i = 0; i < apply.length; i++)
{
if (apply[i].jobId == jobModel[j].jobId)
{
jobModelNew.add(jobModel[j]),
}
}
}
});
}
I want to get all jobs by a list of id. How do I filter it?
There currently is no way to pass in a list of IDs to a Firestore query and get documents matching all those IDs. See Google Firestore - how to get document by multiple ids in one round trip? (which talks about doing this with document IDs), and Firebase Firestore - OR query (which talks about filtering for multiple values on a single field).
Unless your use-case happens to match the workaround mentioned in that second answer, you'll have to perform a separate query for each value, and merge the results in your application code.
Not sure if it is documented anywhere officially, but this is possible now!
.where(admin.firestore.FieldPath.documentId(), "in", [array, of, ids])
Found here: https://stackoverflow.com/a/52252264/10562805
Please take a look at this example. It binds a CollectionReference to a List.
Let me know if this is helpful.

Flutter - Dart : wait a forEach ends

I try to modify a string using data in each node I retrieve from Firebase database, and then to write a file with the modifed string (called "content").
Here is what I tried :
// Retrieve initial content from Firebase storage
var data = await FirebaseStorage.instance.ref().child("...").getData(1048576);
var content = new String.fromCharCodes(data);
// Edit content with each node from Firebase database
final response = await FirebaseDatabase.instance.reference().child('...').once();
response.value.forEach((jsonString) async {
...
// cacheManager.getFile returns a Future<File>
cacheManager.getFile(signedurl).then((file){
// Modify content
content=content.replaceAll('test',file.path);
});
});
// Finally write the file with content
print("test");
final localfile = File('index.html');
await localfile.writeAsString(content);
Result :
"test" is shown before the forEach ends.
I found that we can do in Dart (https://groups.google.com/a/dartlang.org/forum/#!topic/misc/GHm2cKUxUDU) :
await Future.forEach
but in my case if I do : await Future.response.value.forEach (sounds a bit weird)
then I get :
Getter not found: 'response'.
await Future.response.value.forEach((jsonString) async {
How to wait that forEach ends (with "content" modified) before to write the file with new content?
Any idea?
If you use for(... in ) instead of forEach you can use async/await
Future someMethod() async {
...
for(final jsonString in response.value) {
...
// cacheManager.getFile returns a Future<File>
cacheManager.getFile(signedurl).then((file){
// Modify content
content=content.replaceAll('test',file.path);
});
});
}
With forEach the calls fire away for each jsonString immediately and inside it await works, but forEach has return type void and that can't be awaited, only a Future can.
You defined the callback for forEach as async, which means that it runs asynchronously. In other words: the code inside of that callback runs independently of the code outside of the callback. That is exactly why print("test"); runs before the code inside of the callback.
The simplest solution is to move all code that needs information from within the callback into the callback. But there might also be a way to await all of the asynchronous callbacks, similar to how you already await the once call above it.
Update I got working what I think you want to do. With this JSON:
{
"data" : {
"key1" : {
"created" : "20181221T072221",
"name" : "I am key1"
},
"key2" : {
"created" : "20181221T072258",
"name" : "I am key 2"
},
"key3" : {
"created" : "20181221T072310",
"name" : "I am key 3"
}
},
"index" : {
"key1" : true,
"key3" : true
}
}
I can read the index, and then join the data with:
final ref = FirebaseDatabase.instance.reference().child("/53886373");
final index = await ref.child("index").once();
List<Future<DataSnapshot>> futures = [];
index.value.entries.forEach((json) async {
print(json);
futures.add(ref.child("data").child(json.key).once());
});
Future.wait(futures).then((List<DataSnapshot> responses) {
responses.forEach((snapshot) {
print(snapshot.value["name"]);
});
});
Have you tried:
File file = await cacheManager.getFile(signedurl);
content = content.replaceAll('test', file.path);
instead of:
cacheManager.getFile(signedurl).then((file){ ...});
EDIT:
Here's a fuller example trying to replicate what you have. I use a for loop instead of the forEach() method:
void main () async {
List<String> str = await getFuture();
print(str);
var foo;
for (var s in str) {
var b = await Future(() => s);
foo = b;
}
print('finish $foo');
}
getFuture() => Future(() => ['1', '2']);

Resources