I am implementing a notification list and have each item dismissable by swipe.
I would like to add a "Clear All" button that will remove all the notifications one by one in an animation similar to the swipe motion.
Is there a way you can access a dismissable widget to dismiss it programmatically? I could just empty the list and setState, however that wouldn't give me an animation.
My list:
ListView.builder(
itemCount: notifications.length,
itemBuilder: (context, i) {
return Dismissible(
key: Key(notifications[i].hashCode.toString()),
onDismissed: (DismissDirection direction){
onDismissed(notifications[i]);
},
child: Card(
child: ListTile(
leading: Text(DateTime
.now()
.difference(notifications[i].happendAt)
.inMinutes
.toString() +
"m ago"),
subtitle: Text(notifications[i].action),
title: Text(notifications[i].title)),
));
})
Related
The context:
I stumbled upon a minor crash while testing a ListView of Dismissibles in Flutter. When swiping a dismissible, a Dialog is shown using the confirmDismiss option, for confirmation. This all works well, however the UI crashes when testing an unlikely use case. On the page are several options to navigate to other (named) routes. When a dismissible is swiped, and during the animation an option to navigate to a new route is tapped, the crash happens.
How to replicate the crash:
Dismiss the Dismissible
During the animation that follows (the translation of the position of the dismissible), tap on an action that brings you to a
new route. The timeframe to do this is minimal, I've extended it in the example.
The new route loads and the UI freezes
For reference, this is the error message:
AnimationController.reverse() called after AnimationController.dispose()
The culprit is the animation that tries to reverse when it was already disposed:
package:flutter/…/widgets/dismissible.dart:449
Things I've tried:
Initially, I tried checking this.mounted inside the showDialog builder but quickly realised the problem is not situated there.
Another idea was to circumvent the problem by using CancelableOperation.fromFuture and then cancelling it in the dispose() method of the encompassing widget, but that was to no avail.
What can I do solve or at least circumvent this issue?
The code (can also be found and cloned here):
// (...)
class _DimissibleListState extends State<DimissibleList> {
int childSize = 3;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: childSize,
itemBuilder: (context, index) {
if (index == 0) {
return _buildNextPageAction(context);
}
return _buildDismissible();
},
),
);
}
Widget _buildNextPageAction(context) {
return FlatButton(
child: Text("Go to a new page"),
onPressed: () => Navigator.of(context).pushNamed('/other'),
);
}
Dismissible _buildDismissible() {
GlobalKey key = GlobalKey();
return Dismissible(
key: key,
child: ListTile(
title: Container(
padding: const EdgeInsets.all(8.0),
color: Colors.red,
child: Text("A dismissible. Nice."),
),
),
confirmDismiss: (direction) async {
await Future.delayed(const Duration(milliseconds: 100), () {});
return showDialog(
context: context,
builder: (context) {
return Dialog(
child: FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text("Confirm dismiss?"),
),
);
},
);
},
resizeDuration: null,
onDismissed: (direction) => setState(() => childSize--),
);
}
}
I had almost same problem with confirmDismiss ,in my case I was using (await Navigator.push() ) inside of confirmDismiss to navigate to another screen but in return I faced this error :
AnimationController.reverse() called after
AnimationController.dispose()
so to solve my problem inside of confirmDismiss I call a future function out side of confirmDismiss (without await ) and then add return true or false after that function call to finish animation of confirmDismiss.
I am building a simple todo app. I would like to display a edit and delete button in the appbar when user holds on a todo in a ListView. How do I change the appbar and ListView item style like turn background color red.
You should capture the event through the GestureDetector on a ListItem, you can do something like this:
.......
body: new ListView.builder(
itemCount: id == null ? 0 : id.length,
itemBuilder: (BuildContext context, int index) {
return new GestureDetector( //You need to make my child interactive
onTap: () => _selectedItem(id[index]),
child: new Card( //I am the clickable child
child: new Column(
children: <Widget>[
new Text(id[index]),
),
],
),
),
);
}),
....
You can then change color and call the Appbar in method _selectedItem(id[index])
The full page code is very long but my DropdownButton widget code like this.
The problems are,
first: I can't update my selectedCity, it doesn't get an update. Also, the print function calls null, since my cityList data is like [new york, paris, london] etc...
second: flutter doesn't change focus from any TextField to DropdownButton fully. I mean, clicked TextField, then DropdownButton but focus reverts to that TextField after the button click. It is default action of Flutter?
List<dynamic> _cityList;
String _selectedCity;
#override
Widget build(BuildContext context) {
return DropdownButton(
value: _selectedCity,
style: TextStyle(
fontSize: 11,
color: textColor,
),
items: _cityList.map((city) {
return DropdownMenuItem<String>(
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(city),
),
);
}).toList(),
onChanged: (String value) {
setState(() {
_selectedCity = value;
print(_selectedCity);
});
},
isExpanded: true,
);
}
Edit: The solution of resetting FocusNode after selecting an item from DropdownMenuItem is adding this line inside of setstate like this:
this: FocusScope.of(context).requestFocus(new FocusNode());
to here: onChanged:(){setSate((){here}}
I hope it will help you. I have modified your code a little bit
List<dynamic> _cityList;
String _selectedCity;
It will show the Dropdown Button and when you click on it and select any value showing in the print
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
body: ListView(
children: [
Column(
children: <Widget>[
DropdownButton<String>(
items: _cityList.map((dynamic value) {
return DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedCity = value;
print(_selectedCity);
});
},
),
],
),
],
),
);
}
for the focus problem you should use focusNodes one with the drop down list and another with the text field https://docs.flutter.io/flutter/widgets/FocusNode-class.html.
I have a ListView of items in my app and each item of that list is wrapped in a Dismissible widget so as to enable swipe to delete functionality. I also have a FloatingActionButton and on pressing that I navigate to a new page. All the functionality involving swipe to dismiss works fine but if I press the floating action button after dismissing an item, an exception is thrown with the following message:
I/flutter (23062): A dismissed Dismissible widget is still part of the tree.
I/flutter (23062): Make sure to implement the onDismissed handler and to immediately remove the Dismissible
I/flutter (23062): widget from the application once that handler has fired.
Here's the code:
Widget build(BuildContext context) {
final String testVal = widget.testStr;
return Scaffold(
appBar: AppBar(
title: Text('My Device Info'),
),
body: FutureBuilder<AndroidDeviceInfo>(
future: _deviceInfoPlugin.androidInfo,
builder: (BuildContext context, AsyncSnapshot<AndroidDeviceInfo> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
final androidDeviceInfo = snapshot.data;
return ListView(
children: <Widget>[
Dismissible(
key: Key('1'),
background: Container(color: Colors.red,),
onDismissed: (direction){
},
child: ListTile(
leading: Icon(Icons.info),
title: Text('Android build version: ${androidDeviceInfo.version.release}',
style: TextStyle(fontSize: 18.0),),
),
),
ListTile(
leading: Icon(Icons.info),
title: Text('Android device: ${androidDeviceInfo.device}',
style: TextStyle(fontSize: 18.0),),
),
ListTile(
leading: Icon(Icons.info),
title: Text('Android device hardware: ${androidDeviceInfo.hardware}', style: TextStyle(fontSize: 18.0),),
)
],
);
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context){ return new AboutPage();}));
},
child: Icon(Icons.add),
),
);
}
How can I avoid this exception from being thrown? Am I doing something wrong here?
The way you initialize the Key to your Dismissable is wrong because Flutter thinks there can be more Dismissable widgets with the same key value.
Add uuid: ^1.0.3 to your dependencies,
Dismissible(
key: Key(Uuid().v4().toString()), //use the uuid.
.
.
.
I have a code, that uses dismissible in the listview (showes items from database). After dismissing an item it is supposed to show snackbar but it is not showing it and it seems that the dismissible is still part of the tree. Can you help me with that?
return ListView.builder(
itemCount: count,
itemBuilder: (BuildContext context, int position) {
final ThemeData theme = Theme.of(context);
return Dismissible(
key: Key(this.objs[position].id.toString()),
onDismissed: (direction) {
setState(() async {
int result = await helper.delete(this.objs[position].id);
});
Scaffold.of(context)
.showSnackBar(SnackBar(
content: Text(this.objs[position].title + "dismissed")));
},
background: Container(
color: Colors.red,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0)
)
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: getColor(this.objs[position].priority),
child: Text(this.objs[position].id.toString()),
),
title: Text(obj[position].title),
subtitle: Text(objs[position].date),
onTap: () {
debugPrint("Tapped on " + objs[position].id.toString());
navigateToDetail(this.objs[position]);
},
),
);
},
);
this is called inside a Scaffold. And objs is a list that contains all my objects from the database.
Here is my delete code that is called inside onDismissed:
Future<int> delete(int id) async {
Database db = await this.db;
var result = await db.rawDelete("DELETE FROM $tblT WHERE $colId=$id");
return result;
}
I've noticed if I delete one item, and immediately try to create another one (I have an option to insert to DB):
It sometimes throws the error: A dismissed Dismissible widget is still part of the tree
Update:
Moved the delete part, before setState and I am getting the error: A dismissed Dismissible widget is still part of the tree every time I swipe to dismiss
You could try the following code for the onDismissed: property.
The problem is the future inside the onDismissed function. We need to reorder the async and await keywords.
Anyway, take care with the timings when removing successive items.
onDismissed: (direction) async {
String title = this.obj[position].title;
await helper.delete(this.obj[position].id);
setState(() {});
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$title dismissed")));
},
It also moves the async out of the setState() and stores the title to be used later by the SnackBar.
Inside content in SnackBar you can try :
Text(this.obj[position].title.toString() + "dismissed")