A dismissed Dismissible widget is still part of the tree - flutter

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.

Related

Recommended approach to lists with expandable elements in Flutter

I would like to create a list with expandable items/children based on data provided from the stream. Expanded items would include textual details and actions which the user can take.
One of the key requirements for this layout is that - at any time only one list item can be expanded.
From what I've managed to found out - with current out-of-the-box Flutter widgets this needs to be managed programmatically. Once user expands one tile to set values as not-expanded for all other tiles and rebuild UI.
I've managed to make this work in two ways:
Using ListView.builder & ExpansionTile widgets as children
Using ExpansionPanelList & ExpansionPanel widgets as children
Both approached were combined with Riverpod's StateProvider to keep state of the selected/expanded item and invoke rebuild once the state changes (user taps on a item/tile).
Both approaches have its trade-offs from UI perspective - e.g. ExpansionPanelList children padding is poor looking and cannot be modified. Then we have ExpansionTile approach which looses animation once "key" is set with unique PageStorageKey - a necessary config in order for ExpansionTile to save and restore its expanded state (basically programmatic expanding and collapsing of tiles does not work without this...).
What I don't understand is the performance impact of both approached and which one is better in this regard.
How does setting unique key: param and rebuilding ExpansionTile widgets compare ExpansionPanelList and generating list of its items (List<ExpansionPanel>) with each widget rebuild - in order to define which Panel is expanded and which is not.
Intuitively it seems that ListView.builder & ExpansionTile approach is better - but I am not sure what is the impact of setting "key:". Please help in understanding which way is better performance wise.
You can see bellow implementation for both approaches.
ListView.builder & ExpansionTile approach
return ListView.builder(
padding: EdgeInsets.all(10),
itemCount: data.length,
itemBuilder: ((context, index) {
return ExpansionTile(
key: PageStorageKey("${DateTime.now().millisecondsSinceEpoch}"),
initiallyExpanded: (selectedItem == data[index].id.toString()),
onExpansionChanged: ((value) =>
ref.read(selectedProductListItem.notifier).state = (value == true) ? data[index].id.toString() : null),
tilePadding: EdgeInsets.all(10),
leading: Icon(Icons.account_box),
title: Text(data[index].name, style: TextStyle(fontWeight: FontWeight.bold)),
childrenPadding: EdgeInsets.all(10),
expandedAlignment: Alignment.center,
children: [
Text(data[index].description),
SizedBox(height: 10),
ElevatedButton(onPressed: () {}, child: Text('Apply Now')),
],
);
}),
);
ExpansionPanelList & ExpansionPanel approach
final List<ExpansionPanel> listData = [];
for (var i = 0; i < data.length; i++) {
listData.add(
ExpansionPanel(
headerBuilder: (context, isExpanded) {
return Padding(padding: EdgeInsets.all(10), child: Text(data[i].name));
},
body: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Text(data[i].description),
SizedBox(height: 10),
ElevatedButton(onPressed: () {}, child: Text('Apply Now')),
],
),
),
isExpanded: (data[i].id == selectedItem),
canTapOnHeader: true,
),
);
}
return SingleChildScrollView(
padding: EdgeInsets.all(15),
child: ExpansionPanelList(
children: listData,
expansionCallback: (panelIndex, isExpanded) {
ref.read(selectedProductListItem.notifier).state = isExpanded ? null : data[panelIndex].id;
},
),
);

How to know if the writes in Firebase are on the server or only cached?

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.

Flutter. Multiple heroes share same tag. ListView with buttons

Can't figure it out how to solve this problem. So, here is the simplest code which represents my problem:
Scaffold(
body: Stack(
children: <Widget>[
...
Scrollbar(
child: ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: _mapBloc?.mapData?.companies?.count ?? 0,
itemBuilder: (context, index) {
final company = _mapBloc?.mapData?.companies?.data[index];
return InkWell(
child: Hero(
tag: company.id,
child: Card(
child: Container(
height: 50,
width: double.infinity,
),
),
),
onTap: () {
Navigator.of(context)
.pushNamed('/company', arguments: company)
.then(
(results) {
if (results is PopWithResults) {
PopWithResults popResult = results;
}
},
);
},
);
},
),
)
],
),
);
And stack trace:
The following assertion was thrown during a scheduler callback:
There are multiple heroes that share the same tag within a subtree.
Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), each Hero must
have a unique non-null tag.
...
The items count in ListView changes with every database request. If the size is up to 5 widgets in ListView, then clicking works correctly, as soon as the list expands to, for example, 8 elements, I get the error written above. With what it can be connected? I tried to use unique Hero tags, but this does not solve the problem.
I need some advice and hope you can help me. If you need more information, please, write the comment.
Thanks for your attention!

Persistent draggable bottom sheet in Flutter

I'm using Rubber library at the moment, do you know an approach without using 3rd parts libraries?
The Bottom Sheet must be persistent (not dismissable, not triggered by any button instead, always displayed) and draggable (It must be expanded and collapsed by dragging gestures)
Perhaps DraggableScrollableSheet could work?
I haven't yet tried it out myself, but maybe you could fiddle with a listview to make it work.
I'm guessing something like having it's child be a listview, and then limit both the max child size and the maximum scroll extent
If you do not care that the bottom sheet must snap to different positions you can use the widget from the following package (snapping_sheet) I made.
Or, if you do not want to use it as a 3rd part library, you can copy the code and use it as your own widget from repository here: Github - Snapping sheet
Use DraggableScrollableSheet. Here's an example:
Stack(
children: [
Container(), //the page under the DraggableScrollableSheet goes here
Container(
height: MediaQuery.of(context).size.height,
child: DraggableScrollableSheet(
builder: (BuildContext context, myscrollController) {
return Container(
color: Colors.blue,
child: ListView.builder(
controller: myscrollController,
itemCount: 40,
itemBuilder:(BuildContext context, int index) {
return ListTile(title: Text('Item $index',
style: TextStyle(color: Colors.black),
));
},
),
);
},
),
),
],),

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.