Flutter Firebase ListView - Slow Refreshes - firebase

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);
}
);
}
)
),
]
),
),
);
}
}

Related

Firebase doesn't work cause of null-safety (DART/FLUTTER)

I'm using/learning Firebase for my database works. My snapshot's coming like _jsonQuerySnapshot or _jsonDocumentSnapshot. But it had to be QuerySnapshot or DocumentSnapshot. Because of this I have to encode and decode my snapshot for use my datas.
If I'm not using encode decode json I'm getting null or object errors all the time.
Here is my class extends from state
class _MyHomePageState extends State<MyHomePage> {
final _firestore = FirebaseFirestore.instance;
#override
Widget build(BuildContext context) {
CollectionReference moviesRef=_firestore.collection('movies');
DocumentReference babaRef = _firestore.collection('movies').doc('Baba');
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(
title: Text('FireStore Crud'),
),
body: Center(
child: Container(
child: Column(
children: [
StreamBuilder<QuerySnapshot>(
stream: moviesRef.snapshots(),
builder: (BuildContext context,AsyncSnapshot asyncSnapshot){
List<DocumentSnapshot>listOfDocumentSnapshot=asyncSnapshot.data.docs;
return Flexible(
child: ListView.builder(
itemCount: listOfDocumentSnapshot.length,
itemBuilder: (context,index){
Text('${listOfDocumentSnapshot[index].data()['name']}' ,style: TextStyle(fontSize: 24),);
},
),
);
},
),
],
),
),
),
);
}
}
and this is my error .
First of all, check your data is null or not and then use [] on it. Probably, listOfDocumentSnapshot[index].data() is null. If it is null, render another UI such as loading screen. Namely, your loading screen must be showed until reach the data.
for example:
builder: (BuildContext context,AsyncSnapshot asyncSnapshot){
List<DocumentSnapshot>? listOfDocumentSnapshot = asyncSnapshot.data.docs;
if(!listOfDocumentSnapshot.hasData || listOfDocumentSnapshot == null){
return LoadingScreen(); //etc.
}
return Flexible(
child: ListView.builder(
itemCount: listOfDocumentSnapshot.length,
itemBuilder: (context,index){
Text('${listOfDocumentSnapshot[index].data()['name']}' ,style: TextStyle(fontSize: 24),);
},
),
);
},
Futures (asynchronous programmes) need some time to get data and you have to make your UI wait until you get your data. e.g. database connections, read/write somethings to/from somewhere etc.
For more detail you can read this article.

Is it possible to add containers programmatically to a pageview list in flutter?

i wanted my list of pageview containers to be created according to the number of documents in a collection in my cloud Firestore so that when clicked i can display data from each document on each page, is there a way to do this,thanks
You can use a StreamBuilder with a PageView.builder inside as the builder. For example, let Object be the type of the documents you are getting from Firestore:
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: StreamBuilder<List<Object>>(
stream: firestoreService.getObjectsList, // something that returns an Object
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(child: CircularProgressIndicator());
} else {
return Scrollbar(
child: PageView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
// return something for each object in each page
}),
);
}
}),
));
}

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.

SetState is causing Futurebuilder to reload the data on every tap

I am using future builder and stream builder to fetch data from firebase and show them on screen.
I have favourite button as well. when I click on favourite_borderLine iconButton. It fetch data from firebase then change the state to favourite_border iconButton.
It also change the state of every other listview.Builder what I want is just to change the icon state on every click not fetching the whole data from database.
This is the initial state
when I tap on favourite icon, Suppose I tapped on first icon then it start loading.
and then all the icons are changed :(
I just want to change the clicked icon state not all icons and do not want the fetch data on click just change the state of button.Here is code.
class TalentScreen1 extends StatefulWidget {
#override
_TalentScreen1State createState() => _TalentScreen1State();
}
class _TalentScreen1State extends State<TalentScreen1> {
bool toggle = false;
#override
Widget build(BuildContext context) {
return BlocProvider<TalentFavCubit>(
create: (context) => TalentFavCubit(),
child: SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text('Talent Screen 1 home search'),
_retriveAllDocs,
],
),
),
),
),
),
);
}
Widget get _retriveAllDocs => FutureBuilder<QuerySnapshot>(
future: FirebaseRepo.instance.fetchWorkerFormFieldsData(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (!snapshot.hasData) {
return Text("Nothing to show");
}
if (snapshot.connectionState == ConnectionState.done) {
final List<DocumentSnapshot> data = snapshot.data.docs;
return theUserInfo(data);
}
return Text("loading");
});
Widget theUserInfo(List<DocumentSnapshot> data) {
return ListView.builder(
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (context, index) {
return FutureBuilder<DocumentSnapshot>(
future: fetch(data[index]['uid']),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.done) {
TalentHireFavModel userData = TalentHireFavModel.fromMap(
data[index].data(), snapshot.data.data());
return Card(
child: Column(
children: <Widget>[
Text(userData.name),
Text(userData.categories),
Text(userData.skills),
Text(userData.country),
Text(userData.phoneNo),
Text(userData.hourlyRate),
Text(userData.professionalOverview),
Text(userData.skills),
Text(userData.expert),
Text(userData.createdAt),
IconButton(
icon: toggle
? Icon(Icons.favorite_border)
: Icon(
Icons.favorite,
),
onPressed: () {
setState(() {
// Here we changing the icon.
toggle = !toggle;
});
}),
],
),
);
}
return Container();
});
});
}
//TODO: Implementation Fix Error
Widget _iconButton(uid) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseRepo.instance.fetchCurrentUserFavourites().snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
var data = snapshot.data.docs;
// print(snapshot.data.get('uid'));
if (snapshot.hasError) {
return Text('Something went wrong');
}
return IconButton(
icon: data.isEmpty == uid
? Icon(Icons.favorite)
: Icon(Icons.favorite_border),
onPressed: () =>
BlocProvider.of<TalentFavCubit>(context).addTalentFav(uid));
},
);
}
Future<DocumentSnapshot> fetch(data) async =>
await FirebaseRepo.instance.fetchWorkerUserData(data);
}
This is your broken line of code:
future: FirebaseRepo.instance.fetchWorkerFormFieldsData(),
The FutureBuilder documentation starts with:
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.
A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
And you broke the contract. I have a video that illustrates this in detail. https://www.youtube.com/watch?v=sqE-J8YJnpg
Do what the docs say. TL;DR: Do not create the Future in the parameter to FutureBuilder.

How to show a text widget when a screen is not calling data from Firebase

I have the activity screen that gets data from Firebase, I'm trying to show a text message like "No activities yet" when the database is not providing data.
I tried to use a ternary operator condition that checks if the database is empty and it works, however, the problem is that when I go the activity screen and it has data, it still shows the message for about 1 second and then shows the activity screen, so, I would like to know if there is a better way to implement this.
Hope this makes sense.
See the code below
#override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: () => _setupActivities(),
child: _activities.isEmpty
? Center(
child: Text(
'Aun no Tienes Notificaciones',
style: ktextTitlesStyle.copyWith(
color: Colors.black,
fontSize: responsive.height(context) / 50),
),
)
: ListView.builder(
itemCount: _activities.length,
itemBuilder: (BuildContext context, int index) {
Activity activity = _activities[index];
return _buildActivity(activity);
},
),
),
);
}
}
I'm not sure I fully understand what you are trying to accomplish but checking for .hasData on the AsyncSnapshot might help determine if data is returned.
Container( child: FutureBuilder(future: someFutureFirebaseDataReturningMethod(), builder: (context, snapshot) {
if (!snapshot.hasData) return _noActiviesYet();
return _showActivies(snapshot, context);
}))
.hasData returns "whether this snapshot contains a non-null data value."(https://api.flutter.dev/flutter/widgets/AsyncSnapshot/hasData.html)

Resources