I want to use the BottomNavigationBar with multiple tabs, each of them should preserve its state. As each tab is containing a list of items based on a JSON parsing from the web, I have to deal with multiple futures.
At the moment I have the following approach:
BottomNavigationBar with IndexedStack, the index looks like this
static final List<Widget> _pages = <Widget>[
FutureBuilder<List<Photo>>(
future: fetchPhotos(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('Error on Tab 1'),
);
} else if (snapshot.hasData) {
return PhotosList(photos: snapshot.data!);
} else {
return Center(
child: CircularProgressIndicator(color: Colors.orange),
);
}
},
),
FutureBuilder<List<Onboardvideo>>(
future: fetchOnboardvideos(http.Client()),
builder: (context, vidsnapshot) {
if (vidsnapshot.hasError) {
return Center(
child: Text('Error on Tab 2'),
);
} else if (vidsnapshot.hasData) {
return OnboardvideosList(onboardvideos: vidsnapshot.data!);
} else {
return Center(
child: CircularProgressIndicator(color: Colors.orange),
);
}
},
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 15),
child: Text(
"Work in progress"
, style: optionStyle)),
];
What confuses me here is that my code is sometimes executed without any issues and sometimes I get the "Error on Tab 2" message. Tab 1 and 3 are always loaded without issues.
I start to suspect that the IndexedStack does not work well with two FutureBuilders in a row , so that it works sometimes and sometimes not. Maybe because the app is not waiting for both futures.
I did some research and it seems to be possible to load both futures within one FutureBuilder. But I think that is the wrong approach in my case. The best would be to preserve the state just like within an IndexedStack, but to load each page seperately with its own FutureBuilder as soon as the according button on the BottomNavigationBar is tapped. The app would be more efficient then as well and I would save some API quotas on tab 2.
I have found some articles telling to use AutomaticKeepAliveClientMixin, but the next article says I should never do this but use IndexedStack as it is much better.
So I am definitely confused now how I can archive my necessary combination. I am quite new to flutter so it would be great if you could share an example of your recommendation. Thanks!
Related
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 built an Agro App, where the majority of users are Offline when they register the data and when they return to the central site they obtain an Internet connection and the data goes up.
However, it is not clear to the user whether or not his record was uploaded to the cloud, so I would like to implement a tick system similar to that used by WhatsApp:
Gray tick when the data is written and is only in cache
Blue tick when the data uploads to the cloud and therefore is available to other users
What I imagine is something like this:
The procedure with which I display the list is as follows:
Widget _crearListado(BuildContext context, AlimentarBloc alimentarBloc) {
return Column(
children: <Widget>[
Container(
child: Padding(
child: StreamBuilder(
stream: alimentarBloc.alimentarStream,
builder: (BuildContext context, AsyncSnapshot<List<AlimentarModel>> snapshot){
if (snapshot.hasData) {
final alimentarList = snapshot.data;
if (alimentarList.length == 0) return _imagenInicial(context);
return Container(
child: Stack(
children: <Widget>[
ListView.builder(
itemCount: alimentarList.length,
itemBuilder: (context, i) =>_crearItem(context, alimentarBloc, alimentarList[i]),
],
),
);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center (child: Image(image: AssetImage('assets/Preloader.gif'), height: 200.0,));
},
),
),
),
],
);
}
Widget _crearItem(BuildContext context, AlimentarBloc alimentarBloc, AlimentarModel alimentar) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
Container(
child: Card(
child: ListTile(
leading: Container(,
child: Text(" ${alimentar.nombreRefEstanque}"),
),
title: Text('${alimentar.nombreAlimentoFull}'),
subtitle: Container(
child: Container(
child: Text ('$_fechaAlimentar)
),
),
onTap: () => null,
trailing: Container(
child: Text("${alimentar.consumo),
),
)
),
],
);
}
What options do you see to mark the data when they have already uploaded to the Internet? Can I do that?
Unfortunately the Firebase Realtime Database does not have a built-in marker on a data snapshot to indicate whether it's been synchronized to the server.
The simplest approach to implement something like this is to add a completion listener to the write operation, and mark the write as completed when this listener is invoked. This only works while the app remains active however. If the app is restarted, your data will be synchronized later, but no completion handler will be invoked.
If you also want to handle that case, you could write a marker value into the database when the app starts, and add a completion listener for that too. Once the completion listener for the marker value is written, you know that all writes that were queued up before that were also processed by the server.
You could combine the two approaches and:
Keep a set of outstanding write operations in local storage.
Add the key of each item that you write.
Remove the key for an item when its completion handler is called.
Clear the entire list when he app is restarted and your marker value is confirmed.
By the way: this is one area where Cloud Firestore (the other database that is part of Firebase) has a much better API, as it has a hasPendingWrites property that indicates if there are pending writes on the snapshot.
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.
I am building a mobile application using mvp in flutter. The problem i am facing is that the code i used to make the product screen and order screen is same, using stream builder. But in case of Product screen the code is working perfectly fine showing circular Progress indicator but when i navigate to order screen it first show me an error on order screen instead of circular progress indicator and after some milli seconds when it get the response it shows the result. I also attached the video here for better understanding. https://www.youtube.com/watch?v=X4QX1-sKS9k
The init state method and build method of stateful widget are also same but i am not getting the problem where i am doing wrong in the code. The code of build state of order screen is below which is causing problem.
Build method of order:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.orange,
automaticallyImplyLeading: false,
title: Text(
'Orders',
style: TextStyle(color: Colors.white),
),
),
body: widget.myList.isNotEmpty //TODO update it remove the parameters
? orderListView(widget.myList)
: StreamBuilder(
stream: orderPresenter.getOrders,
builder: (context, AsyncSnapshot<OrdersList> snapshot) {
if (snapshot.hasData) {
return orderListView(
snapshot.data.orders //TODO update it remove the parameters
);
} else if (snapshot.data.orders.isEmpty) {
return Center(child: Text('Your orders are empty'),);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(
child: Container(
child: CircularProgressIndicator(),
),
);
},
),
);
}
In Order Presenter we have
final _ordersFetcher = PublishSubject<OrdersList>();
#override
Observable<OrdersList> get getOrders => _ordersFetcher.stream;
Solution that worked for me:
if (!snapshot.hasData) {
return Center(
child: Container(
child: CircularProgressIndicator(),
),
);
}else
if(snapshot.hasData){
orderListView(snapshot.data
.orders);
}
You aren't checking for a case in which snapshot.data.orders could be null. That's what the error in your video is showing. Check for null and show the CircularProgressIndicator() while it is null. You can add it to your first if and it is null it will default to your already present CircularProgressIndicator() at the end of the StreamBilder:
if (snapshot.hasData || snapshot.data.orders != null) {
return orderListView(
snapshot.data.orders //TODO update it remove the parameters
);
}
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.