How to wait for Streambuilder to return a value before coming off a loading screen - flutter

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?

Related

pointless Api requests occuring in future builder flutter

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

how to use two future builder in one page in flutter

In my Flutter app, I need to display two lists that are coming from the database, but I am having trouble getting both lists to display on the same screen. I am using two FutureBuilder widgets, but the first list is displaying correctly while the second list is still loading.
Here is the code I am using:
var future1 = FutureBuilder<List<QuranTextModel>>(
future: database.getQuranText(),
builder: (context, snapshot) {
if(snapshot.hasData){
return ScrollablePositionedList.builder(
itemScrollController: scrollToIndex,
itemCount: snapshot.data!.length,
initialScrollIndex: widget.position,
itemBuilder: (context, index) {
// Build the list item widget here
});
}else{
return const Center(child: CircularProgressIndicator(),);
}
}
);
var future2 = FutureBuilder<List<UrduTextModel>>(
future: database.getUrduTranlation(),
builder: (context, snapshot) {
if(snapshot.hasData){
return ScrollablePositionedList.builder(
itemScrollController: scrollToIndex,
itemCount: snapshot.data!.length,
initialScrollIndex: widget.position,
itemBuilder: (context, index) {
// Build the list item widget here
});
}else{
return const Center(child: CircularProgressIndicator(),);
}
}
);
Column(
children: [
SizedBox(
height: 200,
child: future1,
),
SizedBox(
height: 200,
child: future2,
),
],
)
The first FutureBuilder is used to build a list of QuranTextModel objects, and the second FutureBuilder is used to build a list of UrduTextModel objects. I am using a Column widget to display both lists, with each list contained within a SizedBox widget to give it a fixed height.
The issue I am having is that only the first list is displaying correctly, while the second list is still loading. How can I get both lists to display on the same screen?
Thank you for any help you can provide!
SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: 200,
child: future1),
SizedBox(height: 200,child: future2,)
],
),
),
Try this.
also you have to check your future status before populate you can check that by using
if (snap.connectionState == ConnectionState.done) { your code. you can check does snpa has data in it. }
connection state has deferent states that can help you to make your UI more interactive

Flutter Chat Reply to Message

I'm trying to build a Chat UI with ListView. My first version was a ListView.build that works fine.
I want to upgrade my chat view for "replyTo message" option (similar to whatsapp). This requires the user to be able to click the "replyTo" message and scroll backwards to the original message. The ListView.build does not allow to scroll up to a message that is outside the screen view (as it is not even built).
body: Column(
children: [
Expanded(
child: EasyRefresh.custom(
...
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
ChatMessage _currMsg = vm.chat[index]!;
...
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
...
SwipeTo(
onRightSwipe: () {
setState(() {
isReplying = true;
replyMessage = _currMsg;
});
},
child: ShowMessage(
key: Key(_currMsg.id!),
msg: _currMsg,
myMessage: _currMsg.postById == vm.user!.uid,
onTapReply: (id) async {
final ctx = _itemKey.currentContext;
if (ctx != null)
await Scrollable.ensureVisible(ctx);
},
),
),
],
);
},
childCount: vm.chat.length,
),
),
],
),
),
It seems like the only way is to build all the list items at first build itself.
My question is, is there a way I can retain the already built items, and only add new incoming items rather than rebuilding all items every time.
Thanks.

A dismissed Dismissible widget is still part of the tree

There seem to be many questions regarding this error but I'm yet to find an answer that will work in my situation.
The behaviour I'm seeing is that the Dismissible works, it fires and deletes the item, but for a moment it shows an error in the ListView. I'm guessing it's waiting for the tree to update based on the Stream<List>, which in turn is removing the record from Firebase.
My StreamBuilder...
return StreamBuilder<List<Person>>(
stream: personBloc.personsByUserId(userId),
builder: (context, snapshot) {
...
}
My ListView.builder()
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
var person = snapshot.data[index];
return GestureDetector(
onTap: () {
Navigator.of(context)
.pushNamed('/reading/${person.personId}');
},
child: Dismissible(
key: Key(person.personId),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
personBloc.deletePerson(person.personId);
},
background: Container(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(
FontAwesomeIcons.trash,
color: Colors.white,
),
Text(
'Delete',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.right,
),
],
),
),
color: Colors.red,
),
child: AppCard(
//Bunch of properties get set here
),
),
);
},
My deletePerson
deletePerson(String personId) async {
fetchPersonId(personId).then((value) {
if (value.imageUrl.isNotEmpty) {
removeImage();
}
db.deletePerson(personId);
});
}
I've tried changing the onDismissed to a confirmDismiss with no luck.
Any suggestions?
This happens when you dismiss with a Dismissible widget but haven't removed the item from the list being used by the ListView.builder. If your list was being stored locally, with latency not being an issue, you might never see this issue, but because you are using Firestore (I assume, based on your mention ofFirebase) then there is going to be some latency between asking the item to be removed from the DB and the list getting updated on the app. To avoid this issue, you can manage the local list separately from the list coming from the Stream. Updating the state as the stream changes, but allowing you to delete items locally from the local list and avoiding these kind of view errors.
I ended up making a couple of changes to my code to address this.
I added a BehaviourSubject in my bloc to monitor whether the delete was taking place or not. At the beginning of the firestore delete I set this to true and then added a .then to the delete to set it back to false.
I then added a Streambuilder around the ListView on the screen to monitor the value of this and show a CircularProgressIndicator when true.
It now looks like this:
Thanks for your help.

UI doesn't fully update when receive bloc informations

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.