Flutter/Firestore: How to add items to stream on scroll (preserve scrollposition when done fetching)? - firebase

I have a chat (ListView) with messages that I only want to load as needed.
So when the chat is initially loaded I want to load the last n messages and when the user scrolls up I want to fetch older messages also.
Whenever a new message arrives in the firebase collection it should be added to the ListView. I achieved this by using a StreamBuilder that takes the stream of the last n messages where n is a variable stored in the state that I can increase to load more messages (it is an argument to the function that gets the stream of the last n messages).
But with my current implementation the problem is that even though more messages are fetched and added to the listview when I scroll up, it then immediately jumps back to the bottom (because the listview is rebuilt and the scrollposition isn't preserved). How can I prevent this from happening?

This issue is not related to ListView or the scroll position. Those are kept with automatically. The issue must be somewhere else in your code. Check my example below to see how having a list, adding new items and then resetting it, will maintain the scroll position or move to the right place:
class ListViewStream60521383 extends StatefulWidget {
#override
_ListViewStream60521383State createState() => _ListViewStream60521383State();
}
class _ListViewStream60521383State extends State<ListViewStream60521383> {
List<String> _itemList;
#override
void initState() {
resetItems();
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: ListView.builder(
reverse: true,
itemCount: _itemList.length,
itemBuilder: (context, index){
return Container(
height: 40,
child: Text(_itemList[index]),
);
},
),
),
Row(
children: <Widget>[
RaisedButton(
onPressed: addMoreItems,
child: Text('Add items'),
),
RaisedButton(
onPressed: resetItems,
child: Text('Reset items'),
)
],
)
],
);
}
void addMoreItems(){
int _currentListCount = _itemList.length;
setState(() {
_itemList.addAll(List.generate(60, (index) => 'item ${index + _currentListCount}'));
});
}
void resetItems(){
setState(() {
_itemList = List.generate(60, (index) => 'item $index');
});
}
}

Using FirestoreListView you do that easily.
Refer this for more info https://www.youtube.com/watch?v=si6sTuVZxtw

Related

Incorrect use of parent Widget

I am trying to make a flash Chat App that retrieves the chats from fireBase and displays it on the Screen .I have wrapped it under an Expanded widget .I have give some padding to it .
I am getting the following error
The following assertion was thrown while looking for parent data.:
Incorrect use of ParentDataWidget.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flashchat1/constants.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ChatScreen extends StatefulWidget {
static String id='Chat_Screen';
#override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _fireStore = FirebaseFirestore.instance;//an instance of fireBase store that stored data created
final _auth = FirebaseAuth.instance;//instance/object of fireBase auth that authorizes users is created
late User loggedInUser;//LoggedInUser is of type FireBase user(now changed to user)
late String messageText;
#override
void initState()
{
super.initState();
getCurrentUser();//calling the getCurrentUser
}
void getCurrentUser()
async{
try
{
final user= await _auth.currentUser;//get the current user id/name/email.Also currentUser return a future so make it async by adding await and async keywords
if(user!=null)
{
loggedInUser=user ;//LoggedInUser = user contains email of the info
print(loggedInUser.email);
}
}
catch(e)
{
print(e);
}
}// Under collection there is documents.Inside documents there are fields like type ,values etc.These fields contain our information
Future<void> messageStream()//Using a stream it becomes very easy .U just need to click once after you run the app .Then u will be done.
async {//The snapShot here is FireBase's Query SnapShot
await for(var snapshot in _fireStore.collection('messages').snapshots()){//make a variable snapshot to store the entire items of the collection in fireBase (Look at the fireBase console there is a collection called messages).This collection takes the snapshot of all the iteams (not literal snapshot .Think it like a snapShot)
for(var message in snapshot.docs)//make a variable message to access the snapShot.docs .(docs stands for Documentation.Look at the fireBase console)
print(message.data());
}
}
void getMessages()//(The problem with this is that we need to keep clicking on the onPressed button every single time the new message is sent .So it is not convinient
async {
final messages = await _fireStore.collection('messages').get();//to retrieve the data from fire base we are creating a variable message
messages.docs;//retreive the data from document section under the collection in firestore
for(var message in messages.docs)//since it is a messages.docs is a list we need to loop through it
{
print(message.data());//print the data its messge.data()
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: null,
actions: <Widget>[
IconButton(
icon: Icon(Icons.close),
onPressed: () {
messageStream();
//_auth.signOut();
//Navigator.pop(context);
//Implement logout functionality
}),
],
title: Text('⚡️Chat'),
backgroundColor: Colors.lightBlueAccent,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: StreamBuilder(
stream:_fireStore.collection('messages').snapshots(),
builder: (context, AsyncSnapshot snapshot) {
//This is Flutter's Async snapShot
//if(!snapshot.data)
// {
// return Center(
//child: CircularProgressIndicator(
//backgroundColor:Colors.lightBlueAccent,
//),
//);
//}
if(!snapshot.hasData){//flutters async snapshot contains a query snapshot
return Center(
child:CircularProgressIndicator(
backgroundColor:Colors.lightBlueAccent,
),
);
}
final messages = snapshot.data.docs;
List<Text> messageWidgets = [];
for(var message in messages)//Loop through the messages
{
final messageText = message.data()['text'];//retrieve the data under the text field in message collection
final messageSender = message.data()['Sender'];//retrieve the data under the Sender field in message collection
final messageWidget = Text('$messageText from $messageSender',
style:TextStyle(
fontSize:50,
),
);
messageWidgets.add(messageWidget);//add the text to the List messageWidget
}
return Expanded(
flex:2,
child: ListView(//changed from Column to ListView as we want to scroll down .Or else only finite messages can be fit
children: messageWidgets, //if u don't write else with a return it will show an error as null returned and null safety broken
padding: EdgeInsets.symmetric(horizontal: 5,vertical: 5),
),
);
},
),
),
Container(
decoration: kMessageContainerDecoration,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
onChanged: (value) {
messageText=value;//Whatever you chat will be stored in the variable String variable messageText
},
decoration: kMessageTextFieldDecoration,
),
),
FlatButton(
onPressed: () {
_fireStore.collection('messages').add({
'text': messageText,//add the messages sent to fireStore under the messages object that we created manually
'Sender': loggedInUser.email,//add the current users email to the sender field
},);
},//goal is to send the data that we type here to the fireStore cloud
child: Text(
'Send',
style: kSendButtonTextStyle,
),
),
],
),
),
],
),
),
);
}
}
return Expanded(
flex:2,
child: ListView(//changed from Column to ListView as we want to scroll down .Or else only finite messages can be fit
children: messageWidgets, //if u don't write else with a return it will show an error as null returned and null safety broken
padding: EdgeInsets.symmetric(horizontal: 5,vertical: 5),
),
);
This code block is the issue here. You cannot use Expanded widget anywhere you like. The Expanded widget can only be used inside Row or Column Widget.
Remove the Expanded widget in the above code block. It will works.

Can't get actual String download url from Firebase Storage and only returns Instance of 'Future<String>' even using async/await

I am trying to get user avatar from firebase storage, however, my current code only returns Instance of 'Future<String>' even I am using async/await as below. How is it possible to get actual download URL as String, rather Instance of Future so I can access the data from CachedNewtworkImage?
this is the function that calls getAvatarDownloadUrl with current passed firebase user instance.
myViewModel
FutureOr<String> getAvatarUrl(User user) async {
var snapshot = await _ref
.read(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
if (snapshot != null) {
print("avatar url: $snapshot");
}
return snapshot;
}
getAvatarURL is basically first calling firebase firestore reference then try to access to the downloadURL, if there is no user data, simply returns null.
Future<String> getAvatarDownloadUrl(String code) async {
Reference _ref =
storage.ref().child("users").child(code).child("asset.jpeg");
try {
String url = await _ref.getDownloadURL();
return url;
} on FirebaseException catch (e) {
print(e.code);
return null;
}
}
I am calling these function from HookWidget called ShowAvatar.
To show current user avatar, I use useProvider and useFuture to actually use the data from the database, and this code works with no problem.
However, once I want to get downloardURL from list of users (inside of ListView using index),
class ShowAvatar extends HookWidget {
// some constructors...
#override
Widget build(BuildContext context) {
// get firebase user instance
final user = useProvider(accountProvider.state).user;
// get user avatar data as Future<String>
final userLogo = useProvider(firebaseStoreRepositoryProvider)
.getAvatarDownloadUrl(user.code);
// get actual user data as String
final snapshot = useFuture(userLogo);
// to access above functions inside of ListView
final viewModel = useProvider(myViewModel);
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: snapshot.data, // **this avatar works!!!** so useProvider & useFuture is working
),
),
SizedBox(height: 32),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Center(
child: Column(
children: [
SizedBox(
height: 100,
width: 100,
child: Avatar(
avatarUrl: viewModel
.getAvatarUrl(goldWinners[index].user)
.toString(), // ** this avatar data is not String but Instance of Future<String>
),
),
),
],
),
);
},
itemCount: goldWinners.length,
),
Avatar() is simple statelesswidget which returns ClipRRect if avatarURL is not existed (null), it returns simplace placeholder otherwise returns user avatar that we just get from firebase storage.
However, since users from ListView's avatarUrl is Instance of Future<String> I can't correctly show user avatar.
I tried to convert the instance to String multiple times by adding .toString(), but it didn't work.
class Avatar extends StatelessWidget {
final String avatarUrl;
final double radius;
final BoxFit fit;
Avatar({Key key, this.avatarUrl, this.radius = 16, this.fit})
: super(key: key);
#override
Widget build(BuildContext context) {
print('this is avatar url : ' + avatarUrl.toString());
return avatarUrl == null
? ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image.asset(
"assets/images/avatar_placeholder.png",
fit: fit,
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: CachedNetworkImage(
imageUrl: avatarUrl.toString(),
placeholder: (_, url) => Skeleton(radius: radius),
errorWidget: (_, url, error) => Icon(Icons.error),
fit: fit,
));
}
}
Since the download URL is asynchronously determined, it is returned as Future<String> from your getAvatarUrl method. To display a value from a Future, use a FutureBuilder widget like this:
child: FutureBuilder<String>(
future: viewModel.getAvatarUrl(goldWinners[index].user),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return snapshot.hashData
? Avatar(avatarUrl: snapshot.data)
: Text("Loading URL...")
}
)
Frank actually you gave an good start but there are some improvements we can do to handle the errors properly,
new FutureBuilder(
future: //future you need to pass,
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, i) {
DocumentSnapshot ds = snapshot.data.docs[i];
return //the data you need to return using /*ds.data()['field value of doc']*/
});
} else if (snapshot.hasError) {
// Handle the error and stop rendering
GToast(
message:
'Error while fetching data : ${snapshot.error}',
type: true)
.toast();
return new Center(
child: new CircularProgressIndicator(),
);
} else {
// Wait for the data to fecth
return new Center(
child: new CircularProgressIndicator(),
);
}
}),
Now if you are using a text widget as a return statement in case of errors it will be rendered forever. Incase of Progress Indicators, you will exactly know if it is an error it will show the progress indicator and then stop the widget rendering.
else if (snapshot.hasError) {
}
else {
}
above statement renders until, if there is an error or the builder finished fetching the results and ready to show the result widget.

Make FirebaseAnimatedList auto scroll when have new data

I've try to build Chat example on Flutter, but I have problem, how I can make FirebaseAnimatedFlutter auto scroll when have new data populate ?
Example: When I submit new chat message for my friend, from my side, I can call this method to auto scroll:
Timer(Duration(milliseconds: 100), () {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut);
});
But at my friend side, he still need manual scroll to end to see new message
So, there are anyway to detect and auto scroll to end of FirebaseAnimatedList when we receive new data ?
Thank you
I can't see all your code, but there is a trick you can do that will avoid having to add extra code. It involves reversing the data in the list of messages and setting to true the reverse property of the ListView. This will make the messages move up as new messages come in.
You reverse the original list, you set to true the reverse property of the ListView, and when you add messages to your List you use messages.insert(0, newMessage) to add it to the top (now bottom because of inversion), instead of of messages.add.
class Issue65846722 extends StatefulWidget {
#override
_Issue65846722State createState() => _Issue65846722State();
}
class _Issue65846722State extends State<Issue65846722> {
List<String> messages = [
'message 1',
'message 2',
'message 3',
].reversed.toList();
TextEditingController textEditingController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StackOverflow'),
),
floatingActionButton: Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 100.0),
child: FloatingActionButton(
// To simulate an incoming message from another source that is not
// the local TextField
child: Icon(Icons.message),
onPressed: () => newMessage('new message'),
),
),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index){
return Container(
child: Text(messages[index]),
);
},
),
),
Divider(color: Colors.black,),
TextFormField(
controller: textEditingController,
onFieldSubmitted: (_) => submitMessage()
),
],
),
);
}
void submitMessage(){
newMessage(textEditingController.text);
textEditingController.clear();
}
void newMessage(String newMessage){
setState(() {
messages.insert(0, newMessage);
});
}
}
thank for useful answer of João Soares, i already solve this problem by 2 step
Reverse data from Firebase by use 'sort' property of FirebaseAnimatedList
Set 'reverse' property to 'true' in FirebaseAnimatedList
And work like a charm
FirebaseAnimatedList(
query: loadChatContent(context, app),
sort: (DataSnapshot a,DataSnapshot b) => b.key.compareTo(a.key), //fixed
reverse: true, //fixed
Just wrap your FirebaseAnimatedList with Flexible Widget & thats it.
This worked for me.

Flappy search bar : suggestions not updated after Firebase modifications

I am quite new to flutter, and I have some issues using this package : https://pub.dev/packages/flappy_search_bar
I am using it with suggestions (made when nothing is written in the search bar), and I have different issues with it, due to the fact that the suggestion list does not update when changes append on Firebase.
Here is my code (in a stateful widget):
SearchBarController _searchBarController=SearchBarController();
List<DocumentSnapshot> documents =[];
List<LibraryBook> books = [];
#override
void initState() {
super.initState();
FireHelper().libraryBooksFrom(widget.user.uid).listen((event) {
setState(() {
books=getLibraryBooks(documents);
});
});
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FireHelper().libraryBooksFrom(widget.user.uid),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(snapshot.hasData) {
documents = snapshot.data.docs;
books=getLibraryBooks(documents);
return Scaffold(
backgroundColor: white,
floatingActionButton: FloatingActionButton(
onPressed: () => setState((){
AlertHelper().addBookToLibrary(context);
}),
child: Icon(Icons.add),
backgroundColor: pointer,
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
body: Column(
children: <Widget>[
SafeArea(
bottom: false,
child: Row(
children: <Widget>[
BackButton(color: Colors.black),
MyText(" Ma bibliothèque", color: baseAccent)
],
)),
Expanded(child: SearchBar(
searchBarPadding: EdgeInsets.symmetric(horizontal: 10),
onSearch: (inputText) => searchBooks(inputText),
suggestions: books,
minimumChars: 2,
crossAxisCount: 2,
onCancelled: () => searchBooks(null),
crossAxisSpacing: 0,
onError: (error) => ErrorWidget(error),
searchBarController: _searchBarController,
hintText: "Chercher un livre...",
cancellationWidget: Text("Annuler"),
emptyWidget: Text("Aucune correspondance"),
onItemFound: (item, int index) {
if(item is LibraryBook){
return BookLibraryTile(item, null);
} else {
return Text("Aucune correspondance");
}
}
)
)
],
),
);
} else {
return LoadingCenter();
}
});
}
When I have some changes on Firebase, the list List<LibraryBook> books is well updated, but the suggestions of the searchBar does not follow this update...
Any idea ?
This is what the screen looks like
EDIT :
first issue when cancelling a search
second issue when deleting an item
third issue when adding a new item
(this one does not append every time... i don't know why)
What you need is a state management solution. I suggest checking out the Provider package. You need a single model for the books that is shared between widgets.
Also, check out this Flutter article on state management if you are not familiar.

Flutter Firebase ListView - Slow Refreshes

I created a ListView that populates from a Firebase collection by using a StreamBuilder widget. It takes some time for the ListView to populate because I'm running tasks (HTTP requests) for each item of the Firebase collection and then displaying the result in the list.
When I navigate away from the page with the ListView and then return to the page (using PageView), the ListView appears to refresh entirely instead of using the last seen version. So there is a ~5 second circular progress indicator while the list re-populates every time the page is re-opened.
Questions:
What is the best way to make this ListView not complete a full 5
second refresh every time the page is re-opened? Can it use the last seen version and only update when items are added to the firebase collection?
If I were to remove the tasks (HTTP requests) that need to be ran on each item of the collection and instead simply show values directly from the Firebase collection, should the refresh time be fast enough that it is not a problem?
Is it best to create a local database (using sqflite) that syncs with the Firebase collection to prevent slow refreshes?
Code:
class AccountsPage extends StatefulWidget {
#override
_AccountsPageState createState() => _AccountsPageState();
}
class _AccountsPageState extends State<AccountsPage> {
User user;
Widget _buildListItem(BuildContext context, DocumentSnapshot document, String uuid) {
// get data from firebase
String token = document.data.values.toList()[0];
// For current document/token, make an HTTP request using the token and return relevant data
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: FutureBuilder(
future: anHTTPrequest(token, uuid),
builder: (context, projectSnap) {
if (projectSnap.connectionState == ConnectionState.none ||
!projectSnap.hasData || projectSnap.data.length == 0) {
return Container();
}
return ListView.builder(
shrinkWrap: true,
itemCount: projectSnap.data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(projectSnap.data[index]),
);
},
);
},
),
)],
);
}
#override
Widget build(BuildContext context) {
final container = StateContainer.of(context);
user = container.user;
return Container(
child: Scaffold(
body: Column(
children: <Widget>[
new Flexible(
child: StreamBuilder(
stream: Provider.of(context).collectionRef.document(user.uuid).collection('tokens').snapshots(),
builder: (context, snapshot){
if (!snapshot.hasData){
return Container(
child: Center(
child: Text("No data")
)
);
}
return ListView.builder(
padding: EdgeInsets.all(8.0),
reverse: false,
itemCount: snapshot.data.documents.length,
itemBuilder: (context, int index) {
return _buildListItem(context, snapshot.data.documents[index], user.uuid);
}
);
}
)
),
]
),
),
);
}
}

Resources