I have a Future Builder in my flutter app and it displays --
Error : if there's an error in json parsing
Data : if everything goes smooth
Loader : if its taking time
Everything works. the Future is calling a 'future' function thats doing a get request of some student data and the 'builder' is displaying it. I have an edit dialog box on the same page. I can edit the student information through the put request. The problem is that when I click on the form fields in the edit dialog box, I notice that get request is automatically happening approx 10 times. When I save the edits, a confirmation dialog box appears that data is updated. While this happens again get requests happens upto 10 times. And then it pops. So there are round about 20 useless requests happening on the server.
I think it happens because when I click the form fields the keyboard appears and the underlying displaying widget rebuilds, calling the api. When data is edited keyboards goes back into its place again widget rebuilds, calling the api. How can I resolve this issue ?
this is the code if it helps :
child: FutureBuilder(
future: APIs().getStudentDetails(),
builder: (context, data) {
if (data.hasError) {
return Padding(
padding: const EdgeInsets.all(8),
child: Center(child: Text("${data.error}")));
} else if (data.hasData) {
var studentData = data.data as List<StudentDetails>;
return Padding(
padding: const EdgeInsets.fromLTRB(0, 15, 0, 0),
child: SingleChildScrollView(
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.9,
child: ListView.builder(
itemCount: studentData.length,
itemBuilder: ((context, index) {
final student = studentData[index];
final id = student.studentId;
final father = student.fatherName;
final mother = student.motherName;
final cg = student.cg;
final cityName = student.city;
final studentName = student.studentName;
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(30.0),
child: SingleChildScrollView(
child: GestureDetector(
onDoubleTap: () {
edit(context, id!, studentName!, father,
mother, cg, cityName!);
},
child: Column(children: [
CustomReadOnlyField(
hintText: id.toString()),
CustomReadOnlyField(hintText: studentName),
CustomReadOnlyField(hintText: father),
CustomReadOnlyField(hintText: mother),
CustomReadOnlyField(
hintText: cg.toString()),
CustomReadOnlyField(hintText: cityName),
]),
),
),
),
);
}),
scrollDirection: Axis.vertical,
),
),
),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
I followed this answer and it worke. Flutter FutureBuilder gets constantly called
Apparantly I had to 'Lazily initializing my Future' and 'Initializing my Future in initState:'
Create a state variable for future like
late final future = APIs().getStudentDetails();
and use
FutureBuilder(
future: future ,
You can check Fixing a common FutureBuilder and StreamBuilder problem
class _YourWidgetState extends State<YourWidget> with AutomaticKeepAliveClientMixin<YourWidget> {
#override
bool get wantKeepAlive => true;
So extend your Widget with AutomaticKeepAliveClientMixin so items inside Listview will not be reproduced
Related
I'm trying to create a program where teachers can create classes and other users can request an invitation for those classes. This is the document structure in Firebase:
classes_data (collection)
{class_id} (document)
invitations (collection)
{uid} (document)
users_data (collection)
{uid} (document)
classes_hosting (collection)
{class_id} (document)
I want a screen with a list that displays all of the classes the user is hosting, and inside each class I want a nested list to display all the pending invitations for that class. However, I want the list of invitations to be animated so that the list changes in real-time with an animation every time someone requests an invitation for that class. People can request invitations and cancel requests, so the list has to be able to have the elements added and removed.
The code that I wrote works fine with nested ListViews. However, as soon as I change the inner ListView to an AnimatedList, it stops working properly.
import 'package:classes_app/models/classHosting.dart';
import 'package:classes_app/models/pending_invitations.dart';
import 'package:classes_app/models/user.dart';
import 'package:classes_app/services/database.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Hosting5 extends StatefulWidget {
const Hosting5({Key? key}) : super(key: key);
#override
State<Hosting5> createState() => _Hosting5State();
}
class _Hosting5State extends State<Hosting5> with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
// Stream with all classes that user is hosting and will happen in the future (after today):
var stream1 = DatabaseService(uid: hiphenUserSnapshot!.uid!).futureClassesHosting;
return SingleChildScrollView(
child: StreamBuilder<List<ClassHosting>>(
stream: stream1,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Text("Loading");
}
if (snapshot.data!.isNotEmpty) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 15.0),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
// Stream with all pending invitations for that class:
var stream2 = DatabaseService(uid: hiphenUserSnapshot!.uid!).getPendingInvitationRequests(snapshot.data![index].classId!);
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Text(
"Class: ${snapshot.data![index].title!}",
style: const TextStyle(
fontSize: 16,
color: Color(0xff101010),
fontWeight: FontWeight.w500,
height: 1.0,
),
),
StreamBuilder<List<PendingInvitation>>(
stream: stream2,
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text("Loading...");
if (snapshot.data!.isNotEmpty) {
return AnimatedList( // WORKS FINE WHEN THIS IS A LISTVIEW.BUILDER
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
initialItemCount: snapshot.data!.length,
itemBuilder: (context, index, animation) {
return Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Pending invitation name: ${snapshot.data![index].name}",
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
],
),
],
);
},
);
} else {
return const SizedBox();
}
},
),
],
),
);
}
);
} else {
return const SizedBox();
}
},
),
);
}
}
I get a RangeError (index) most of the time when the list changes.
I know that I still have to provide an animation, but I'm trying to get the rest to work first. I believe this error happens because the AnimatedList has to know which elements to remove and which ones to insert through a listKey. I tried to use stream.listen((){}) and then compare the old list with the new list to remove/add the necessary items like explained in this question. The issue is that I can't figure out how to do this with each event. Basically, since the AnimatedList is nested inside a ListView, I don't think the approach explained in the question work for this case, or at least not without some modifications that I can't figure out.
I would really appreciate if someone could help me make this work.
Question
I have an app that loads a list of habits as a Stream from Firestore, and I want the app to show a loading screen until habit loading has completed, and then show the list if there is one. Is there a way to keep a loading screen showing until we've either finished loading the first value in the stream or determined there won't be one?
The issue I'm having is that while my app is showing the loading screen, it briefly loads with a "no habits found" view before switching to show the list of habits.
Setup
This view uses three components:
I have a model-view-viewmodel architecture based on the Stacked Package.
(1) My view is a ViewModelBuilder widget with a StreamBuilder inside it. It looks at a (2) DailyViewModel, where the relevant components are an isBusy boolean property and a Stream<List<HabitCompletionViewModel> property that the view's StreamBuilder looks at to display the habits. The habits are loaded from Firestore via a FirestoreHabitService (this is an asynchronous call - will be described in a minute).
The View works as follows:
If DailyViewModel.isBusy is true, show a Loading... text.
If isBusy is false, it will show a Stream of Habits, or the text "No Habits Found" if the stream is not returning any habits (either snapshot.hasData is false, or data.length is less than 1).
#override
Widget build(BuildContext context) {
return ViewModelBuilder.reactive(
viewModelBuilder: () => vm,
disposeViewModel: false,
builder: (context, DailyViewModel vm, child) => vm.isBusy
? Center(child: Text('Loading...'))
: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'BaseDate of ${DateFormat.yMd().format(vm.week.baseDate)}'),
Text(
'Week ${DateFormat.yMd().format(vm.week.beginningOfWeek)} - ${DateFormat.yMd().format(vm.week.endOfWeek)}'),
SizedBox(height: 20),
Row(
children: [
Flexible(
child: Text('Today',
style: Theme.of(context).textTheme.headline6),
),
],
),
const Padding(padding: EdgeInsets.all(2)),
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false ||
snapshot.data == null ||
snapshot.data!.length < 1) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
SizedBox(height: 40),
TextButton(
child: Text('Create a New Habit'),
onPressed: () => vm.navigateToCreateHabitPage(),
),
],
),
),
);
}
}
Process of Loading the Data
My loading happens as follows:
The ViewModel is initialized, and setBusy is set to true.
DailyViewModel({required WeekDates week}) {
setBusy(true);
_log.v('initializing the daily viewmodel');
pageSubtitle =
'Week of ${week.startWeekday.title} ${DateFormat.Md().format(week.beginningOfWeek)}';
_log.v('page subtitle is $pageSubtitle');
mainAsyncCode();
}
Then it starts this mainAsyncCode() method, which gets a stream of habits from the FirestoreHabitService (this returns a Future<Stream<List<Habit>>> because there is a potential modification function performed on the habits before returning them), and once that is completed, transforms that stream into a Stream<List<HabitCompletionViewModel>>, and then sets isBusy on the ViewModel to false.
void mainAsyncCode() async {
_myHabits = await _habitService.loadActiveHabitsByUserFuture(_loginAndUserService.loggedInUser!.id!);
todaysHabits = _myHabits!.transform(currentViewModelCompletion);
await Future.delayed(Duration(seconds: 5));
setBusy(false);
}
Issue
The problem is that there is a temporary delay where the screen goes from "Loading..." to "No Habits Found" before it shows the list of Habits. I want it to wait on "Loading" until the stream list has been published, but I can't think of a way to do that.
Are there any options for doing this that others are aware of?
I have a list of chats, and I want to show on each chat card, if there's a new message that the user hasn't read.
The list is in a StatefulWidget, the list contains refactored cards that are also StatefulWidgets, I also made the code to work with Firestore to check if the user has read the message, but still I don't know what's happening, because it doesn't update the icon of unread messages.
The data changes in the database, but it doesn't in the chat card. If I reload the app, because the cards are rebuilt, then it does change.
Here's the chat card code:
bool hasUnreadMessages = false;
void unreadMessagesVerifier() {
setState(() {
_firestore.collection('chatRoom').document(_chatRoomID).get().then((data) async {
hasUnreadMessages = await data['hasUnreadMessages'];
});
});
}
#override
Widget build(BuildContext context) {
unreadMessagesVerifier();
return GestureDetector(
child: Stack(
children: <Widget>[
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Container(
width: double.infinity,
child: Text(
widget.lastMessage,
),
),
),
hasUnreadMessages
? Container(
margin: EdgeInsets.fromLTRB(10, 0, 5, 0),
child: CircleAvatar(
radius: 7,
backgroundColor: Colors.blue,
),
)
: SizedBox(),
],
),
),
],
),
onTap: widget.onTap,
); // ChatCard
}
If more info is needed, do let me know!
========================================================================
EDIT:
Fixed thanks to #Pedro R.
I just had to move the SetState() and check the mounted
void unreadMessagesVerifier() {
_firestore.collection('chatRoom').document(_chatRoomID).get().then((data) async {
if (mounted) {
setState(() {
hasUnreadMessages = data['hasUnreadMessages'];
});
}
});
}
I think your problem lies in the way you are calling setState.
Try calling it after the future finishes.
Like this:
void unreadMessagesVerifier() {
_firestore.collection('chatRoom').document(_chatRoomID).get().then((data) =>
data['hasUnreadMessages'].then(result){
setState((){
hasUnreadMessages = result;
});
});
}
Sorry for the formatting by the way.
Consider using StatefulBuilder class, it rebuilds the particular Widget which it wraps based upon the value getting updated
So, hasUnreadMessage will be used to update the Container(). Do something like this
StatefulBuilder(
builder: (BuildContext context, StateSetter setState){
// here you return the data based upon your bool value
return hasUnreadMessages ? Container(
margin: EdgeInsets.fromLTRB(10, 0, 5, 0),
child: CircleAvatar(
radius: 7,
backgroundColor: Colors.blue,
)
) : SizedBox();
}
)
I have this weird problem: I want to update a grid of items when I click on it. I use a BLoC pattern to manage the changement so the view just receive a list and have to display it. My problem is that the view doesn't fully update.
Before I go further in the explanation, here my code
body: BlocEventStateBuilder<ShopEvent, ShopState>(
bloc: bloc,
builder: (BuildContext context, ShopState state) {
staggeredTile.clear();
cards.clear();
staggeredTile.add(StaggeredTile.count(4, 0.1));
cards.add(Container());
if (state.products != null) {
state.products.forEach((item) {
staggeredTile.add(StaggeredTile.count(2, 2));
cards.add(
Card(
child: InkWell(
child: Column(
children: <Widget>[
Image.network(
item.picture,
height: 140,
),
Container(margin: EdgeInsets.only(top: 8.0)),
Text(item.title)
],
),
onTap: () {
bloc.emitEvent(ClickShopEvent(item.id));
},
),
),
);
});
}
return StaggeredGridView.count(
crossAxisCount: 4,
staggeredTiles: staggeredTile,
children: cards,
);
}),
So, I have two items. When I click on the first one, I'm suppose to have one item with a different name and picture. But when I click, I have one item as expected, but with the same text and image. When I print thoses values, it's correctly updated but the view doesn't show it.
Do you have any clues of my problem?
For a reason that I can't explain, when I replaced
staggeredTile.clear();
cards.clear();
by
staggeredTile = new List<StaggeredTile>();
cards = new List<Widget>();
It works fine.
If someone can explain me the reason, I'd be gratefull.
I'm working on my first Flutter app (debugging on my Android phone). I have a list with row items. When you long-press the row, it copies the content into the user's clipboard. This is working great!
But I need to let the user know that the content was copied.
I've attempted to follow many tutorials on trying to get the row surrounded by a build method or inside a Scaffold, but I can't get any to work. Is there an alternative method to notifying the user (simply) that something like "Copied!" took place?
Notice the commented out Scaffold.of(... below. It just seems like there must be an easier method to notifying the user other than wrapping everything in a Scaffold. (and when I try, it breaks my layout).
import 'package:flutter/material.dart';
import 'package:my_app/Theme.dart' as MyTheme;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/services.dart';
class RowRule extends StatelessWidget {
final DocumentSnapshot ruleGroup;
RowRule(this.ruleGroup);
_buildChildren() {
var builder = <Widget>[];
if (!ruleGroup['label'].isEmpty) {
builder.add(new Text(ruleGroup['label'],
style: MyTheme.TextStyles.articleContentLabelTextStyle));
}
if (!ruleGroup['details'].isEmpty) {
builder.add(new Text(ruleGroup['details'],
style: MyTheme.TextStyles.articleContentTextStyle));
}
return builder;
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: () {
Clipboard.setData(new ClipboardData(text: ruleGroup['label'] + " " + ruleGroup['details']));
// Scaffold.of(context).showSnackBar(SnackBar
// (content: Text('text copied')));
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 3.0),
child: new FlatButton(
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 0.0),
child: new Stack(
children: <Widget>[
new Container(
margin: const EdgeInsets.symmetric(
vertical: MyTheme.Dimens.ruleGroupListRowMarginVertical),
child: new Container(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0, vertical: 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
),
)),
)
],
),
),
));
}
}
The goal is to have a page like this (see image), which I have, and it works and scrolls...etc, but I cannot get it to work with a Scaffold, and therefore, haven't been able to use the snackbar. Each "Row" (which this file is for) should show a snackbar on longPress.
You can use GlobalKey to make it work the way you want it.
Since I don't have access to your database stuff, this is how I gave you an idea to do it. Copy and paste this code in your class and make changes accordingly. I also believe there is something wrong in your RowRule class, can you just copy the full code I have given you and run?
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
final GlobalKey<ScaffoldState> _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFFFFFF).withOpacity(0.9),
key: _key,
body: Column(
children: <Widget>[
Container(
color: Color.fromRGBO(52, 56, 245, 1),
height: 150,
alignment: Alignment.center,
child: Container(width: 56, padding: EdgeInsets.only(top: 12), decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.yellow)),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: 120,
itemBuilder: (context, index) {
return Container(
color: Colors.white,
margin: const EdgeInsets.all(4),
child: ListTile(
title: Text("Row #$index"),
onLongPress: () => _key.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("Copied \"Row #$index\""))),
),
);
},
),
),
],
),
);
}
}
These is a simple plugin replacement for the Snackbar named "Flushbar".
You can get the plugin here - https://pub.dartlang.org/packages/flushbar
You don't have to take care of any wrapping of widgets into scaffold also you get a lot of modifications for you like background gradient, adding forms and so on into Snackbar's and all.
Inside your onLongPressed in GestureDetectore you can do this.
onLongPressed:(){
Clipboard.setData(new ClipboardData(text: ruleGroup['label'] + " " + ruleGroup['details']));
Flushbar(
message: "Copied !!",
duration: Duration(seconds: 3),
)..show(context);
}
This will display the snackbar in you app where you would want to see it also you can get a lot of modification available to you so the you can make it look as per your app.
There are couple of things you need to do, like use onPressed property of the FlatButton it is mandatory to allow clicks, wrap your GestureDetector in a Scaffold. I have further modified the code so that it uses GlobalKey to make things easy for you.
Here is the final code (Your way)
class RowRule extends StatelessWidget {
final GlobalKey<ScaffoldState> globalKey = GlobalKey();
final DocumentSnapshot ruleGroup;
RowRule(this.ruleGroup);
_buildChildren() {
var builder = <Widget>[];
if (!ruleGroup['label'].isEmpty) {
builder.add(new Text(ruleGroup['label'], style: MyTheme.TextStyles.articleContentLabelTextStyle));
}
if (!ruleGroup['details'].isEmpty) {
builder.add(new Text(ruleGroup['details'], style: MyTheme.TextStyles.articleContentTextStyle));
}
return builder;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: globalKey,
body: GestureDetector(
onLongPress: () {
Clipboard.setData(new ClipboardData(text: ruleGroup['label'] + " " + ruleGroup['details']));
globalKey.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('text copied')));
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 3.0),
child: new FlatButton(
onPressed: () => print("Handle button press here"),
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 0.0),
child: new Stack(
children: <Widget>[
new Container(
margin: const EdgeInsets.symmetric(vertical: MyTheme.Dimens.ruleGroupListRowMarginVertical),
child: new Container(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0, vertical: 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
),
),
),
)
],
),
),
),
),
);
}
}
I made a dropdown banner package on pub that allows you to easily notify users of errors or confirmation of success. It's a work in progress as I continue to add visually rich features.
I am not sure if your build() method is completed or you are yet to change it, because it consist of many widgets which are just redundant. Like there is no need to have Container in Container and further Padding along with a FlatButton which would make complete screen clickable. Also having Column won't be a good idea because your screen may overflow if you have more data. Use ListView instead.
So, if you were to take my advice, use this simple code that should provide you what you are really looking for. (See the build() method is of just 5 lines.
class RowRule extends StatelessWidget {
final GlobalKey<ScaffoldState> globalKey = GlobalKey();
final DocumentSnapshot ruleGroup;
RowRule(this.ruleGroup);
_buildChildren() {
var builder = <Widget>[];
if (!ruleGroup['label'].isEmpty) {
builder.add(
ListTile(
title: Text(ruleGroup['label'], style: MyTheme.TextStyles.articleContentLabelTextStyle),
onLongPress: () {
globalKey.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("Clicked")));
},
),
);
}
if (!ruleGroup['details'].isEmpty) {
builder.add(
ListTile(
title: Text(ruleGroup['details'], style: MyTheme.TextStyles.articleContentTextStyle),
onLongPress: () {
globalKey.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("Clicked")));
},
),
);
}
return builder;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: globalKey,
body: ListView(children: _buildChildren()),
);
}
}
I read your comments on all answers and here is my conslusion:
You need ScaffoldState object that is just above the widget in tree to show Snackbar. You can either get it through GlobalKey as many have suggested. Fairly simple if the Scaffold is created inside build of the widget, but if it is outside the widget (in your case) then it becomes complicated. You need to pass that key, wherever you need it through Constructor arguments of child widgets.
Scaffold.of(context) is a very neat way to just do that. Just like an InheritedWidget, Scaffold.of(BuildContext context) gives you access of the closest ScaffoldState object above the tree. Else it could be a nightmare to get that instance (by passing it through as constructor arguments) if your tree was very deep.
Sorry, to disappoint but I don't think there is any better or cleaner method than this, if you want to get the ScaffoldState that is not built inside build of that widget. You can call it in any widget that has Scaffold as a parent.