Flutter Dismissible insists list item must be removed from tree - dart

I am using a list of Dismissible items and want a swipe in one direction to delete the item but a swipe in the other direction to initiate an edit of the item. However, Flutter insists that a Dismissible item must be removed from the tree in the onDismissed callback. I've tried re-inserting the item but that doesn't work. Any ideas? Extract from the code creating the list items is below:
return new Dismissible(
key: new ObjectKey(item),
direction: DismissDirection.horizontal,
onDismissed: (DismissDirection direction) {
setState(() {
item.deleteTsk();
});
if (direction == DismissDirection.endToStart){
//user swiped left to delete item
_scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text('You deleted: ${item.title}'),
action: new SnackBarAction(
label: 'UNDO',
onPressed: () { handleUndo(item); }
)
));
}
if (direction == DismissDirection.startToEnd){
//user swiped right to edit so undo the delete required by flutter
Async.scheduleMicrotask((){handleUndo(item);});
Navigator.of(context).pushNamed('/tskedit');
}
},
...

You can use confirmDismiss function of Dismissible widget for this purpose.
If you don't want the widget to get dismissed, then you just need to return false from confirmDismiss.
Don't use onDismissed to do your post-swipe processing, use confirmDismiss instead, it will provide you with the swipe direction just as onDismissed.
Here is the official documentation for confirmDismiss function:
Gives the app an opportunity to confirm or veto a pending dismissal.
If the returned Future completes true, then this widget will be
dismissed, otherwise it will be moved back to its original location.
If the returned Future completes to false or null the [onResize]
and here is an example:
Dismissible(
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
/// edit item
return false;
} else if (direction == DismissDirection.endToStart) {
/// delete
return true;
}
},
key: Key(item.key),
child: Text(item.name),
)

The Dismissible will think your item was dismissed as long as the item key changes. Let's say your item class is MyItem. If you implement a constructor MyItem.from in your MyItem class that copies the fields over, e.g.:
class MyItem {
MyItem({ #required this.title, #required this.color });
MyItem.from(MyItem other) : title = other.title, color = other.color;
final String title;
final Color color;
}
Then you can replace handleUndo(item) with handleUndo(new MyItem.from(item)) so that your new ObjectKey(item) will be unique from the old ObjectKey that you used before (assuming you didn't implement operator == on MyItem).

Related

Flutter - How to change the color of an icon through boolean return?

I have a function that checks what books are favorited in the Firestore, the book screen has an star icon, I use isFavorite function to check if is the opened book is favorited by the user or not, if so it returns true and the icon need to change it's color to yellow, if not it returns false and the color need to be black but the color is not changing.
The updateFavorite function works perfectly adding and removing the favorite book from Firestore when touching the icon.
InkWell(
child: Icon(
Icons.star,
size: 30,
color: isFavorite == true ? Colors.yellow
: Colors.black,
),
onTap: (){
model.updateFavorite(model.getUserId(), document.documentID);
},
),
==============
Future<bool> isFavorite() async{
firebaseUser = await _auth.currentUser();
DocumentSnapshot favoritesRef = await Firestore.instance.collection("users")
.document(firebaseUser.uid).get();
if(favoritesRef.data["favorites"].contains(document.documentID)){
return true;
}
else {
return false;
}
}
==============
Future<bool> updateFavorite(Future<DocumentReference> uid, String bookId) async{
firebaseUser = await _auth.currentUser();
DocumentReference favoritesRef = Firestore.instance.collection("users")
.document(firebaseUser.uid);
return Firestore.instance.runTransaction((Transaction tx) async{
DocumentSnapshot postSnapshot = await tx.get(favoritesRef);
if(postSnapshot.exists){
if(!postSnapshot.data["favorites"].contains(bookId)){
await tx.update(favoritesRef, <String, dynamic>{
"favorites": FieldValue.arrayUnion([bookId])
});
// Delete de bookId from Favorites
} else {
await tx.update(favoritesRef, <String, dynamic>{
"favorites": FieldValue.arrayRemove([bookId])
});
}
}
}).then((result){
print(firebaseUser.uid);
print(bookId);
return true;
}).catchError((error){
print("Error: $error");
return false;
});
}
I would recommend you to do not use isFavorite() directly like that from build. The build method might get called constantly and it must have all the data it needs right away. (and the absence of the data must be dealt, for instance, with a loading animation)
Suggestion:
make a property bool isFavorite in your State class
change your method isFavorite() to _loadFavorite()
in _loadFavorite(), instead of returning the value, update the property isFavorite using setState()
call _loadFavorite() in initState()
For the default value for isFavorite, you can either define it on initState(), you can set a default value like false OR keep it null and in your build method make the check, if it is null, then do not show any Icon, for instance (or even a loading icon).
If you want, you can aditionaly listen to the firestore reference to monitor if it gets changed, and if it does, you can update the state using setState() as well.

DropDownButton selected item isn't changing in UI

The objects I want to add in my DropDownButton:
class Car {
int id;
String make;
Car(this.id, this.make);
static List<Car> getCars() {
var cars = new List<Car>();
cars.add(Car(1, "Ford"));
cars.add(Car(2, "Toyota"));
cars.add(Car(3, "BMW"));
return cars;
}
}
Constructing the DropDown (StatefulWidget State class):
class _MyHomePageState extends State<MyHomePage> {
Car _selectedCar;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(child: getDropDown()));
}
Widget getDropDown() {
var cars = Car.getCars();
this._selectedCar = cars.first; // Default to first value in list.
var items = cars.map((car) {
return new DropdownMenuItem<Car>(
value: car,
child: new Text(car.make),
);
}).toList();
return DropdownButton<Car>(
value: this._selectedCar,
onChanged: (Car car) {
setState(() {
this._selectedCar = car;
});
},
items: items);
}
}
DropDownButton Shows up correctly with first item selected, but when I select another item the UI never updates to show the new item as selected.
You need to initalize list just once, because there wont be a match for DropDownList value if you init new list on every draw.
Working example found here: Gist
Try initializing the _selectedCar variable in your initState() method instead of the getDropdown() method.
According to the code you posted, the _selectedCar variable gets reinitialized every time you call setState() since build() method is called.
Also you mentioned you are getting the below error when trying the solution in the first answer:
I/flutter ( 5072): 'package:flutter/src/material/dropdown.dart':
Failed assertion: line 560 pos 15: 'items == null || I/flutter (
5072): items.isEmpty || value == null ||
items.where((DropdownMenuItem item) => item.value == I/flutter (
5072): value).length == 1': is not true.
This is most likely because more than one items in your dropdown is getting the same value.
A possible fix can be using the id parameter of the Car object as the dropdown value instead of the entire object, since the id will be unique for each object. More detail about this error can be found here.

How can I rebuild a widget (with a list) I can't access?

I can't update my debtlist after deleting one of the items.
I have a mainscreen with a bottomnavigationbar with two options. Check the list or a general info of all the items. index zero is the list, so its drawn by default.
https://i.imgur.com/6qOVPPE.png
In my main_screen.dart, I have a List debtList witch I get from a SQLite database. I pass this list into each constructor.
Widget _getTab(int currentIndex) {
switch (currentIndex) {
case 0:
return DebtList(debtList);
case 1:
return GeneralInfo(debtList);
break;
default:
return DebtList(debtList);
}
}
Now I tap on an item an access to that item info.
https://i.imgur.com/ikl4cEI.png
When I click on delete, it works. The item is no longer on the database, but the list is not updated.
dbHelper.deleteDebt(widget.debt.id);
Navigator.pop(context, **true**);
I've tried to return true so I can check it like this:
bool result = await Navigator.push(context, MaterialPageRoute(builder: (context) => DebtDetail(debt)));
if (result != null && result == true) {
...
But the method updateList() is in my main_screen.dart class and not in my debt_list.dart class.
What am I doing wrong? I can't update the list in my debt_list class because its final.
widget.debtList = debtList; //cant do this
What should I change?
Thank you for your time!
You need to notify the DebtList to know which list item Debt to remove. Since you have many actions on DebtDetail screen to update list like update, delete... so it's better to define a DTO (Data Transfer Object) class to pass from DebtDetail to DebtList.
class DebtActionResult {
int action; // 0: nothing, 1: update, 2: delete
int debtId;
Debt updateDebt;
DebtActionResult({this.action, this.debtId, this.updateDebt});
}
Now, on DebtDetail screen, we have to pass the result to DebtList
void _delete() {
DebtActionResult result = DebtActionResult(action: 2, debtId: widget.debt.id, null);
Navigator.pop(context, result);
}
void _update() {
DebtActionResult result = DebtActionResult(action: 1, debtId: widget.debt.id, newDebt); // handle this yourself, because you are adding `final` to the state. Remove the final or create a new `newDebt` state variable.
Navigator.pop(context, result);
}
So far so good, it's time to handle the result on DebtList screen.
Future<void> _navigateToDetail(Debt debt) async {
DebtActionResult result = await Navigator.push(context, MaterialPageRoute(builder: (context) => DebtDetail(debt)));
if (result == null) {
// Do nothing, no change
} else {
if (result.action == 2) {
// oh, it's a delete request
setState( () {
widget.debtList.removeWhere( (d) => d.id == result.debtId );
});
} else if (result.action == 1) {
// modify the debtList here .. write your code similar to delete above.
}
}
}

Use pushNamed with Dismissible Widget

I am trying to create a Dismissible widget but I want to have the router history, I mean when I go to another route using the onDismissed event, when user presses back button on that new view be return to the first one.
This is my widget.
Dismissible(
key: new ValueKey("dismiss_key"),
direction: DismissDirection.horizontal,
child: Container(child: this.getTopPlacesSubscription()),
onDismissed: (direction) {
if (direction == DismissDirection.endToStart) {
Navigator.of(context).pushNamed(Router.getRoute(Routes.map));
}
if (direction == DismissDirection.startToEnd) {
Navigator.of(context)
.pushNamed(Router.getRoute(Routes.camera));
}
}
I will appreciate any help.
I got an issue trying to do it in this way.
This code block will let you create a random string based on length. Add this to your code.
import 'dart:math';
String _randomString(int length) {
var rand = new Random();
var codeUnits = new List.generate(
length,
(index){
return rand.nextInt(33)+89;
}
);
return new String.fromCharCodes(codeUnits);
}
In your state, define a new variable and give it a random value.
String vk;
#override
void initState() {
this.vk = _randomString(10)
}
Then go to your Dismissable widget and replace vk with your string. And here comes the magic part lol. You have to change your vk value in onDismissed. This will pass a new value to Dismissable key, so Flutter will recognize it as a new Widget, which will prevent the error.
Dismissible(
key: new ValueKey(vk),
direction: DismissDirection.horizontal,
child: Container(child: this.getTopPlacesSubscription()),
onDismissed: (direction) {
setState(() {
this.vk = _randomString(10);
});
if (direction == DismissDirection.endToStart) {
Navigator.of(context).pushNamed(Router.getRoute(Routes.map));
}
if (direction == DismissDirection.startToEnd) {
Navigator.of(context)
.pushNamed(Router.getRoute(Routes.camera));
}
}

Flutter - Update parent state from child

I'm not able to update the parent state from a child. The child widget has a callback named 'onDragEnd' which is called once the drag and drop operation on a grid cell is finished. At this time the parent (the grid) should rebuild UI according to the items new sort, but nothing happens. If I set a breakpoint before the 'setState()' the 'elems' property contains the correct items with right sorting. Even if I replace the entire list, UI is not updating. Any suggestions? What am I missing?
class _CustomGridState extends State<CustomGrid> {
List<Pack> elems;
_CustomGridState(this.elems);
#override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
children: this
.elems
.map((pack) => PackFingernail(pack: pack,
onDragEnd: (Pack source, Pack target) {
var sourceIndex = this.elems.indexWhere((p) => p.id == source.id);
var targetIndex = this.elems.indexWhere((p) => p.id == target.id);
this.elems.removeAt(sourceIndex);
this.elems.insert(targetIndex, source);
setState(() {}); // Not working <-----
})
).toList()
);
}
}
Try to add a key for your PackFingernail
// constructor
PackFingernail(..., {Key key}): super(key:key)
// in gridview
.map((pack) => PackFingernail(pack: pack,
onDragEnd: (Pack source, Pack target) {
...
},
key: Key(pack.id.toString()))
).toList()
Key can help to rebuild the GridView

Resources