Show Firebase RealTime Database data in a widget in Flutter - firebase

using firebase realtime database with flutter, saving user details in realtime database & will be displayed in their profile page. I am able to store and get data from database, but the data does not reflect on the screen, I am trying to show the data as an initial value in the text field. How can I show data on profile page when user clicks on the profile?
getUserDetails() async {
final userProfileDetails = await FirebaseDatabase.instance
.reference()
.child("userDetails")
.child(FirebaseAuth.instance.currentUser.uid)
.once()
.then((DataSnapshot snapshot) {
setState(() {
email = snapshot.value['email'];
city = snapshot.value['city'];
state = snapshot.value['state'];
});
});
return data;
}
widget tree -
Scaffold > Column > TextFormFields
TextFormField(
initialValue: email,
onChanged: (value) {
petName = value;
},
decoration: InputDecoration(
suffixIcon: Icon(
Icons.email,
color: grey700,
),
labelText: 'Email'),
),

If you want to see live-changes from the db you meed to make use of the provider & stream principles of flutter.
In short: you are creating a stream which delivers the desired data (a stream is listening to a specific collection/document). So if some data of the stream is changed you get a notification. The notification ends up in the provider you have to wrap a widget with. Every time the stream delivers new data your wrapped widget and everything below gets rebuild.

Related

How to validate Flutter Text Form Field before updating Firestore database

I'm having trouble validating a text form field before data is uploaded to the Firestore database. There aren't any issues with Firestore, only with the way I have written my code.
The validation is to check that the text field is not null or empty, the same as the Flutter codelab. If the text field is empty an error message should appear and the user should remain on the same page until the text field has data. Once the text field is valid the data should be saved to Firestore, a snackbar confirmation should appear on the page and the user should be navigated to a different page.
With the code I have written the user navigates to the new page and the snackbar message appears even if all of the text form fields are empty (invalid). I tried to remove extra code to make it easier to view the code. In my form I have three text fields with identical validation rules. Any help would be greatly appreciated. Thanks.
My Text Form Field:
TextFormField(
labelText: 'Trivia Game Url',
onChanged: (val) {
triviaGameImageURL = val as String;
},
controller: _triviaGameImageURLController,
validator: _triviaGameImageURLValidator,
),
Validation method:
String? _triviaGameImageURLValidator(value) {
if (value == null || value.isEmpty) {
return 'Image Url is required';
}
return null;
}
Method to update Firestore when user taps button:
Future<void> createFirestoreTriviaGame() async {
final form = _formKey.currentState;
if (form!.validate()) {
setState(() {
_isLoading = true;
});
String triviaGameID = const Uuid().v4();
Map<String, dynamic> triviaGameMap = {
FirebaseString.triviaGameID: triviaGameID,
FirebaseString.triviaGameImageURL: triviaGameImageURL,
};
await firestoreMethods
.addTriviaGameData(
triviaGameData: triviaGameMap,
triviaGameID: triviaGameID,
)
.then((value) {
setState(() {
_isLoading = false;
});
});
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(SnackBarString.triviaGameCreated),
),
);
Timer(const Duration(seconds: 2), () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const AddTriviaGamePage(),
),
);
});
}
In your code the snackbar and navigation is outside the if (form!.validate()) block. Simply extend this block to include everything that you want to execute only on a valid form, and display a message if validation fails.
The snackbar and navigation should happen on successful save only, after Firestore async call completes. I recommend not to mix async/await with .then, these are the same. So simple await Firestore method, put this in a try/catch block and execute snackbar and navigation on success.
You need to wrap the textfield in a form widget and assign a form key to the form widget
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
Form(
key: formKey,
child: Column(
children: [
... your text fields
// validate
if (formKey.currentState!.validate()) {

How can I mix a statenotifier, with streamproviders that fetch 2 collections from Firebase to get constant update on a chat list

This is my first question here and I hope I’m not making it too complex.
So, I’m a junior programmer and I’ve start learning flutter, firebase and riverpod a couple of months ago and I’m stuck in a specific project.
For part of the app, what I need to do is something very similar to WhatsApp:
A screen with all the user chats,
This screen (or part of it) should update every time a chat gets a new message, showing the last message snippet and turning into bold if not read,
The shown chats should change between unarchived and archived, depending on a button in the UI.
Couldn’t have picked up an easier starting project, right? ;) (now every time I look at WhatsApp I say wow!)
Regarding firebase/firestore I’m fetching 2 different collections for this:
the sub-collection ‘chats’ within the ‘user_chats’ collection: where I get all the chat Ids plus it’s status (if this chat is archived and if the last message was read), for the current user,
the main ‘chats’ collection, where I have the main info of each chat.
At this moment I’m doing this:
In the chats_screen (UI) I’m fetching my chat provider: userChatsProvider
(note: this is just an example of the UI implementation. I have another implementation for it, but as long I get the chatName and lastMsgContent updated for each chat, perfect.)
class ChatsScreen extends ConsumerWidget {
const ChatsScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
(…)
return Scaffold(
appBar: (…) // not relevant for this question
body: Center(
child: Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
return ref.watch(userChatsProvider).when(
loading: () => const CircularProgressIndicator(),
error: (err, st) => Center(child: Text(err.toString())),
data: (chatData) {
return Column(
children: [
// button to get archived chats
child: TextButton(
child: (…)
onPressed: () {}),
),
Expanded(
child: ListView.builder(
itemCount: chatData.length,
itemBuilder: (ctx, index) => Row(
children: [
Text (chatData[index].chatName!),
Text (chatData[index].lastMsgContent!),
]
),
),
),
]
);
}
);
}
)
)
);
}
}
In the chats_provider (provider) I’m fetching the 2 repository providers and joining them into a specific model I’ve created for this screen:
final userChatsProvider = FutureProvider.autoDispose<List<ChatScreenModel>>((ref) async {
const String userId = ‘XXX’; // this will be substituted by a dynamic variable with the current user Id
try {
final List<UserChat> userChats =
await ref.watch(userChatsRepositoryProvider).get(userId: userId);
final List<String> chatIdList = userChats.map<String>((e) => e.id).toList();
final List<Chat> chats =
await ref.watch(chatsRepositoryProvider).get(chatIds: chatIdList);
// ref.maintainState = true;
return toChatScreenModel(chats, userChats);
} on Exception catch (e) {
throw const CustomException();
}
});
In the repositories I’m fetching the firestorm collections I mentioned above. Here’s an example:
final userChatsRepositoryProvider =
Provider<UserChatsRepository>((ref) => UserChatsRepository(ref.read));
class UserChatsRepository {
final Reader _read;
const UserChatsRepository(this._read);
Future<List<UserChat>> get({required String userId}) async {
try {
final snap = await _read(firebaseFirestoreProvider)
.collection('users_chats/$userId/chats')
.get();
// Maybe this step is not necessary, but I’ve decided to transform the data into temporary models, before sending to provider
List<UserChat> userChats =
snap.docs.map((doc) => UserChat.fromJson(doc.data(), doc.id)).toList();
return userChats;
} on FirebaseException catch (e) {
throw CustomException(message: e.message);
}
}
}
And by the way, this is the model I’m sending to the UI:
class ChatScreenModel {
final String? id;
final String? chatName;
final String? chatImage;
final String? lastMsgContent;
final String? lastMsgDate;
final ChatType? chatType;
final bool? archived;
final bool? msgRead;
ChatScreenModel({
this.id,
this.chatName,
this.chatImage,
this.lastMsgContent,
this.lastMsgDate,
this.chatType,
this.archived,
this.msgRead,
});
Problems with this implementation:
I’m getting the user chats in the screen, but they don’t update since I’m not using a stream. So I get a snapshot, but it will only update if I leave and enter that chats_screen again. And it would be important to have it updating with a stream.
I’m showing all the chats, and not a filtered list with only the unarchived chats.
Also, related with the previous point, I still don’t have the archived button working, to only show the archived chats.
I’ve lost many, many hours trying to understand how I could implement a stream provider and a state notifier provider in this workflow.
Tried many combinations, but without success.
Can anyone help me understand how to do this?
Priority: transform these providers into stream providers (so it updates the UI constantly).
Nice to have: also include the archived/unarchived dynamic to filter the chats that appear and be able to switch between them.
Thanks a lot. :)

Does Flutter Streambuilder use cache on setState or retrieve all documents again from the firestore database?

I am building a chat functionality for my Flutter application.
To ensure that the latest message of a user is always showed, I need the real-time functionality of a Streambuilder.
However, I need to introduce pagination as well to avoid that all chat messages from the database are loaded from the database each time the Widget is rebuilt.
I've implemented this behaviour successfully with the code below. Each time the maximum scrollExtent is reached, the Widget is rebuilt with a higher documentLimit.
My question is now the following:
When setState is called, will the data from the streambuilder that was already there be read again from the database? Or will the Streambuilder use its cache where possible and only use reads for the documents that it does not have?
In other words, with a documentLimit of 20, will each set state only cost me maximum 20 extra reads? Or will it cost me the entire new documentLimit?
void initState() {
maxMessageToDisplay = 20;
_scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
setState(() {
maxMessageToDisplay += 20;
});
}
});
super.initState();
}
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore.collection('chat').limit(maxMessageToDisplay).orderBy('timestamp', descending: true).snapshots(),
builder: (context, snapshot) {
final messages = snapshot.data.documents;
messages.sort((a, b) => b.data['timestamp'].compareTo(a.data['timestamp']));
var format = new DateFormat("Hm");
List<MessageBubble> messageBubbles = [];
for (var message in messages) {
final messageText = message.data['text'];
final messageSender = message.data['sender'];
final messagePhoto = message.data['photo'];
final messageUserId = message.data['uid'];
final messageTime = format.format(DateTime.fromMillisecondsSinceEpoch(message.data['timestamp'], isUtc: false));
final messageBubble = MessageBubble(
sender: messageSender,
text: messageText,
photo: messagePhoto,
time: messageTime,
userId: messageUserId,
);
messageBubbles.add(messageBubble);
}
return Expanded(
child: ListView(
controller: _scrollController,
reverse: true,
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
children: messageBubbles,
),
);
},
);
}
}
By changing the limit you create a whole new query and all data for it is loaded as it is for each new query.
To avoid loading all data you could implement a pagination as shown here. Where you would use the last loaded doc and startAfter to load the next 20 elements from the point where the previous query stoped. It would make your code more complicated to handle multiple realtime queries and keep track of the last docs.
You could also consider to have only the last 20 messages in realtime and if you need the older ones to get them with the get call only once because they would probably not change. Make sure to store all messages from the listener and get calls into a single List and make sure not to store duplicates accidentaly.
I would also consider if it is so importand to avoid loading all data when watching older messages. Does that happen so often in your app? I have multiple chat apps with Firebase that use the same logic with just increasing the limit. Users very rarely watch older messages so implementing such workflows with pagination would not have much effect on the billing and total read amount.

Streambuilder help flutter firebase storage

Hello I’m trying to make a sequence of images with flutter but when I update the photo in firebase Storage the photo in my APP remains the same.
Maybe I can do this with streambuilder but I don’t know how to use it and I can’t learn how to use it.
Does anyone know how I can do that thanks so much for the help.
class _MyHomePageState extends State<MyHomePage> {
final FirebaseStorage storage = FirebaseStorage(
app: Firestore.instance.app,
storageBucket: 'gs:...com/');
Uint8List imageBytes;
String errorMsg;
_MyHomePageState() {
storage.ref().child('images/opencv.png').getData(100000000000000).then((data) =>
setState(() {
imageBytes = data;
})
).catchError((e) =>
setState(() {
errorMsg = e.error;
})
);
}
#override
Widget build(BuildContext context) {
var img = imageBytes != null ? Image.memory(
imageBytes,
fit: BoxFit.cover,
) : Text(errorMsg != null ? errorMsg : "Loading...");
return new Scaffold(
appBar: new AppBar(
),
body: new ListView(
children: <Widget>[
img,
],
));
}
}
Firebase Storage does not automatically notify clients when there's a change to an image they loaded. When you call getData() it gets the data once, and that' the end of it.
If you want to auto-reload the image when it changes in Storage, I recommend using another service to handle that notification.
For example, you could use Firebase Realtime Database or Cloud Firestore to store the path of the image and when it was last modified, and then update that at the same time when you update the image data in Storage. Then your client code would use a realtime listener to the database, and from that load/reload the image.

Firestore replicating a SQL Join for noSQL and Flutter

I realise there is many questions in regards to replicating joins with NoSql document databases such as FireStore, however i'm unable to find a thorough solution utilising Dart/Flutter with FireStore.
I have done some research i feel that in the following example i would be looking for a 'many to many' relationship (please correct me if this is wrong) as there may be a future need to look at all profiles as well as all connections.
In firebase, i have two root level collections (profile & connection):
profile
> documentKey(Auto Generated)
> name = "John Smith"
> uid = "xyc4567"
> documentKey(Auto Generated)
> name = "Jane Doe"
> uid = "abc1234"
> documentKey(Auto Generated)
> name = "Kate Dee"
> uid = "efg8910"
connection
> documentKey(Auto Generated)
> type = "friend"
> profileuid = "abc1234"
> uid = "xyc4567"
> documentKey(Auto Generated)
> type = "family"
> profileuid = "abc1234"
> uid = "efg8910"
For this example the 'connection' documents have been created hypothetically for the user John Smith (uid: xyc4567) when he connected to Jane Doe (uid: abc1234) and Kate Dee (uid: efg8910).
Here is the relational SQL i'm looking to replicate to show a list of profiles which John Smith has connected with:
Select * FROM profile, connection
WHERE profile.uid = connection.profileuid
AND profile.uid = "xyc4567"
In flutter my flutter app i have a fireStore query starting point:
stream: Firestore.instance.collection('profile')
.where('uid', isEqualTo: "xyc4567").snapshots(),
Obviously it only returns from one collection. How do i join the collections in a many to many relationship?
Unfortunately, there is no JOIN clause in Cloud Firestore nor in others NoSQL databases. In Firestore queries are shallow. This means that they only get items from the collection that the query is run against. There is no way to get documents from two top-level collection in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use properties of documents in a single collection.
So the most simple solution I can think of is to query the database to get the uid of a user from the profile collection. Once you have that id, make another database call (inside the callback), and get the corresponding data that you need from the connection collection using the following query:
stream: Firestore.instance.collection('connection').where('uid', isEqualTo: "xyc4567").snapshots(),
Another solution would be to create a subcollection named connection under each user and add all connection objects beneath it. This practice is called denormalization and is a common practice when it comes to Firebase. If you are new to NoQSL databases, I recommend you see this video, Denormalization is normal with the Firebase Database for a better understanding. It is for Firebase realtime database but same rules apply to Cloud Firestore.
Also, when you are duplicating data, there is one thing that need to keep in mind. In the same way you are adding data, you need to maintain it. With other words, if you want to update/detele an item, you need to do it in every place that it exists.
Suppose, you want to use a Stream that depends on some Future objcets.
Stories
Document ID (Auto Generated) //Suppose, "zddgaXmdadfHs"
> name = "The Lion & the Warthog"
> coverImage = "https://...."
> author = "Furqan Uddin Fahad"
> publisDate = 123836249234
Favorites
Document ID (Auto Generated)
> storyDocID = "zddgaXmdadfHs" //Document ID of a story
> userId = "adZXkdfnhoa" //Document ID of a user
Sql equivalent query should look like this
SELECT * FROM Favorites AS f, Stories AS s
WHERE f.storyDocID = s.DocumentID
AND f.userId = user.userId
And Firestore query like this
final _storeInstance = Firestore.instance;
Stream <List<Favorite>> getFavorites() async* {
final user = await _getUser(); //_getUser() Returns Future<User>
yield* _storeInstance
.collection('Favorites')
.where('userId', isEqualTo: user.userId)
.snapshots()
.asyncMap((snapshot) async {
final list = snapshot.documents.map((doc) async {
final story = await _getStory(doc['storyDocID']);
return Favorite.from(doc, story); //Favorite.from(DocumentSnapshot doc, Story story) returns an instance of Favorite
}).toList(); //List<Future<Favorite>>
return await Future.wait(list); //Converts List<Future<Favorite>> to Future<List<Favorite>>
});
}
Future<Story> _getStory(String storyDocID) async {
final docRef = _storeInstance
.collection('Stories')
.document(storyDocID);
final document = await docRef.get();
final story = Story.from(document);
return story;
}
I did some like this to join results from two colections objects and categories.
i did two StreamBuilders to show in a list, in the first one i got the categories and put in a map, then i query the objects and get the category object from the map using the categoryID:
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('categoryPath')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> categorySnapshot) {
//get data from categories
if (!categorySnapshot.hasData) {
return const Text('Loading...');
}
//put all categories in a map
Map<String, Category> categories = Map();
categorySnapshot.data.documents.forEach((c) {
categories[c.documentID] =
Category.fromJson(c.documentID, c.data);
});
//then from objects
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('objectsPath')
.where('day', isGreaterThanOrEqualTo: _initialDate)
.where('day', isLessThanOrEqualTo: _finalDate)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> objectsSnapshot) {
if (!objectsSnapshot.hasData)
return const Text('Loading...');
final int count =
objectsSnapshot.data.documents.length;
return Expanded(
child: Container(
child: Card(
elevation: 3,
child: ListView.builder(
padding: EdgeInsets.only(top: 0),
itemCount: count,
itemBuilder: (_, int index) {
final DocumentSnapshot document =
objectsSnapshot.data.documents[index];
Object object = Object.fromJson(
document.documentID, document.data);
return Column(
children: <Widget>[
Card(
margin: EdgeInsets.only(
left: 0, right: 0, bottom: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(0)),
),
elevation: 1,
child: ListTile(
onTap: () {},
title: Text(object.description,
style: TextStyle(fontSize: 20)),
//here is the magic, i get the category name using the map
of the categories and the category id from the object
subtitle: Text(
categories[object.categoryId] !=
null
? categories[
object.categoryId]
.name
: 'Uncategorized',
style: TextStyle(
color: Theme.of(context)
.primaryColor),
),
),
),
],
);
}),
),
),
);
I'm not sure if is what you want or is clear but i hope it help you.
I think denominational should not be preferred because to maintain the it you have to make extra writes to firestore
instead jorge vieira is correct since you are allowed to make double reads as compare to the writes
so its better to read twice instead of writing writing data twice and its also very impractical to remember every demoralized thing in a large project

Resources