Flutter: how to access context from Dismissible onDismissed - flutter

I'm trying to implement undo for a Dismissible list item in Flutter, and having problems accessing a BuildContext.
I have a flutter list, where each item is a card. The card is wrapped in a Dismissible, which allows the user to swipe to dismiss the card. Dismissible automatically removes the item from the list. Dismissible also has an onDismissed event - I'm using this event to update the item in Redux state store (setting an isDismissed flag to true), then show a snackBar which contains an UNDO button.
This is where I'm running into problems. I want the UNDO button to restore the item, by dispatching another action to the Redux store to set isDismissed to false. To do this I need a context, from which to get the store dispatcher. However when I try with the below code, I get an error when clicking on UNDO:
Looking up a deactivated widget's ancestor is unsafe
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard(this.product);
#override
Widget build(BuildContext context) {
return Dismissible(
key: Key(product.id.toString()),
onDismissed: (direction) {
StoreProvider.of<AppState>(context).dispatch(DismissAction(product));
// Then show a snackbar to allow undo
Scaffold.of(context).showSnackBar(
SnackBar(
content: Row(
children: <Widget>[
Expanded(child: Text("Dismissed ${product.title}"),),
FlatButton(
onPressed: () {
// THIS IS WHERE I GET THE ERROR
StoreProvider.of<AppState>(context).dispatch(UndoDismissAction(product));
},
child: Text("UNDO"),
)
],
)
)
);
},
child: Card(
child: ...
)
);
}
}
From what I've read, I think what is going on is that the line StoreProvider.of<AppState>(context) inside the undo button's onPressed action is trying to use a context which belongs to the Card, but because the card has been removed from the list, it no longer exists.
I'm not sure how to do work around this. I've read about flutter keys, and think the answer may be to start passing around some kind of global key, but I can't quite get my head around how that works. I gave it a go and ran into another problem with 'inheritFromWidgetOfExactType' was called on null. Are keys the solution to this problem? If so where do I create the key, do I pass it in to the widget, what type of key should I use etc, or is there a better solution?
Many thanks!

Extract a single copy of the store into a local variable, which will then get captured by all the lambdas below.
#override
Widget build(BuildContext context) {
var store = StoreProvider.of<AppState>(context);
return Dismissible(
...
store.dispatch(DismissAction(product));

Related

Custom Event listeners in flutter

I have a widget with a list and a button with a tree dot icon in every row that shows and hides a panel in its own row. I only want one panel open in the list. When I click on a row button, I'd like to close the panels of the other rows list.  All the buttons in the list are siblings. I'd like to send an event to the other rows' code to close the panels. Which is the correct manner of flutter?  
I have tried NotificationListener but it does not work because the components to be notified are not their parents.
The question is if the correct thing to do is to use the event_listener library or to use streams. I'm new to flutter/dart and streams seem too complex to me. It's a very simple use case and in this entry
Flutter: Stream<Null> is allowed?
they say
*
Some peoples use streams as a flux of events instead of a value
changing over time, but the class isn't designed with this in mind.
They typically try to represent the following method as a stream:
So with simple events with 0 or 1 argument. event_listener or Streams?
This is the screen I'm working on. I want that when one yellow button panel opens the other one closes.
Your question is broad and it seems to be a design question, i.e. it doesn't have a right answer.
However, I don't think you should use Streams or EventListeners at all in this case, because you should not make components in the same layer communicate with each other. Components should only communicate with their parents and children, otherwise your code will increase in complexity really fast. That's even documented in flutter_bloc.
Other than that, if you don't lift state up, i.e. move the responsibility of triggering the removal of the other rows to a parent Widget, than you're fighting against Flutter instead of letting it help you.
It's easy to create a parent Widget, just wrap one Widget around it. What you want to do is hard, so why would try to communicate with sibling widgets instead of using what's Flutter designed to do?
This is a suggestion:
class _NewsSectionState extends State<NewsSection> {
Widget build(BuildContext context) {
return ListView.builder(
itemCount: newsInSection.length;
itemBuilder: (_, int index) => NewsTile(
title: Text('${newsInSection[index].title}')
onDismiss: () => onDismiss(index),
// I don't know how you set this up,
// but () => onDismiss(Index)
// should animate the dismiss of the Row with said index
),
);
}
}
class NewsRow extends StatefulWidget {
final void Function() onDismiss;
#override
State<NewsRow> _createState => _NewsRowState();
}
class _NewsRowState extends State<NewsRow> {
Widget build(BuildContext context) {
return Row(
children: [
// title
// home button
// fav button
// remove button
IconButton(
Icons.close,
onPressed: widget.onDismiss,
),
],
);
}
}

How can I get the index of a widget in a list?

I can a little bit confuse on how should I implement the delete function.
By tapping the bottom button will add a Custom Card widget to the widget list that shows up on the top
This is the function for that
onTap: () {
setState(() {
_variationCards.add(VariationCard(
key: UniqueKey(),
variation: Variation.empty,
index: _variationCards.length,
onRemoveCard: (index) {
setState(() {
_variationCards.removeAt(index);
});
},
));
});
},
The problems of the current setup are
Lets say I delete the second Card, now that the third Card became the second Card, but the index I passed to the third Card widget is still 2 when it is actually 1 now.
I need the index in the Card widget because I need to have additional widget for the first card, but now If I delete the first card, the second card index value will not be updated.
I think I am heading to the wrong path to implement this. Any help would be much appreciated.
Thank you in advanced.
Maybe you could use "removeWhere" method. Something like:
onTap: () {
_variationCards.add(VariationCard(
key: UniqueKey(),
variation: Variation.empty,
index: _variationCards.length,
onRemoveCard: (index) {
setState(() {
_variationCards.removeWhere((card) => card.SomeId == _someId);
});
},
));
},
The leading information should rather be a list of your defined class / model instead of widgets. You want widgets to update (rebuild) according to the underlying information / state in your app. Currently your widgets are created and added to your list on gestures (like onTap) and won't update themselves - that's why the indices are "wrong" in your case.
I would suggest a combination of a class / model representing a card while making use of the ListView.builder variant:
Class representing your card:
/// Currently only holds the Variation enum value, add whatever
/// defines your cards later on
class VariationModel {
Variation variation;
VariationModel(this.variation)
}
Your widget which displays the cards in a list:
/// Need to be filled with instances of this class, maybe passed
/// down directly to this widget or grabbed from something like
/// GetIt or Provider etc.
List<VariationModel> _variations;
#override
Widget build(BuildContext context) {
...
ListView.builder(
/// +1 to include the button to add a new card at the end
itemCount: _variations.length + 1,
itemBuilder: (context, index) => index < _variations.length ?
VariationCard(
key: UniqueKey(),
variation: _variations[index].variation,
index: index,
onRemoveCard: () {
setState(() {
_variations.removeAt(index);
});
},
/// Add your widget (card) here which is used to add a new
/// new card (in this case a new VariationModel instance)
) : Card(
...
onTap: () {
setState(() {
_variations.add(VariationModel(Variation.empty));
});
},
),
),
}
Since this approach is using the ListView.builder widget which builds the children (your card widgets) dynamically and the index is properly being updated since the widgets are going to rebuild now, it should work as you intend to.
I made a little app using GetX with your requirements (I think so at least), it maybe be of help to you:
The code is here.

How do I keep my widget state when reordering them via Draggable&DragTarget?

I'm using this reorderables package. This package works by having a list of children widgets that are each wrapped with a Draggable and put inside a DragTarget. Before that the childs key is assigned to a GlobalObjectKey.
After the dragTarget is created, it is assigned(or rebuild?) to a KeyedSubTree:
dragTarget = KeyedSubtree(key: keyIndexGlobalKey, child: dragTarget);
According to the comments in the package source code, this should preserve the child widgets state (toWrap) when being dragged:
// We pass the toWrapWithGlobalKey into the Draggable so that when a list
// item gets dragged, the accessibility framework can preserve the selected
// state of the dragging item.
final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap.key);
The reordering itself happens not with the DragTarget accepting the Draggable dragged into it, but rather by using the DragTarget around each child to get index of the current position the Draggable is hovering over. When the Draggable is let go, a reorder function will get called, which removes the widget (that was being dragged) from the list and inserting it into the new position.
Now comes my problem: The state of the widget is not being preserved. I made a simple TestWidget to test this:
class TestWidget extends StatefulWidget{
#override
_TestWidgetState createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
Color boxColor;
#override
void initState() {
super.initState();
boxColor= Colors.blue;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Container(
decoration: BoxDecoration(color: boxColor),
child: Text("Test"),
),
FlatButton(
onPressed: (){
setState(() {
boxColor = Colors.red;
});
},
padding: EdgeInsets.all(8.0),
child: Text("Change to Red"),
color: Colors.grey,
)
],
);
}
}
This widget has a Container with a initial blue background (boxColor) and a button. When the button is pressed, it will change the boxColor to red. The moment the dragging on the widget is initiated, it is rebuild and defaults to the initial state (at least the Draggable feedback is). After the reordering that doesn't change and the widget is still in it's default state.
My plan here is to have a list of different custom widgets, where the User can modify their content and if they are not happy with the order, they can drag those widgets around and rearrange them.
My question is: How do I preserve the state of my widgets?
I'm thinking of creating a class for each widget with all state relevant variables and use that to build my widgets but that seems very bloated and not really in the mind of flutter. Isn't that supposed to be the role of the state of the StatefulWidget?
EDIT:
So I solved my problem by creating an additional class for my widget state with ChangeNotifier and then moving all my variables that I want to keep track of into this class. So I basically now have two lists, one for my widgets in the reorderable list and one for their states. I still think that this is kinda scuffed. If a widget in my list has additional children of its own, I would need to create separate state classes for each of them that need it and save them somewhere. This can get very messy, very quickly.

Flutter - Keep page static throughout lifecycle of app?

I have created an AppDrawer widget to wrap my primary drawer navigation and reference it in a single place, like so:
class AppDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
title: new Text("Page1"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => Page1.singleInstance));
}
),
new ListTile(
title: new Text("Page2"),
trailing: new Icon(Icons.arrow_right),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new Page2("Page 2")));
}
),
]
),
);
}
}
I have also created a custom AppScaffold widget, which simply returns a consistent AppBar, my custom AppDrawer, and body:
class AppScaffold extends StatelessWidget {
final Widget body;
final String pageTitle;
AppScaffold({this.body, this.pageTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: new Text(pageTitle), backgroundColor: jet),
drawer: AppDrawer(),
body: body
);
}
}
I have created two pages: Page1, and Page2. They are simple right now, and look something like this:
class Page1 extends StatelessWidget {
final String pageText;
Page1(this.pageText);
static Page1 get singleInstance => Page1("Page1");
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeCustomWidget())
],
)
),
);
}
}
class Page2 extends StatelessWidget {
final String pageText;
Page2(this.pageText);
#override
Widget build(BuildContext context) {
return AppScaffold(
pageTitle: this.pageText,
body: SafeArea(
child: Stack(
children: <Widget>[
Center(child: SomeOtherCustomWidget())
],
)
),
);
}
}
When I run my app, I can see the navbar and drawer correctly. I can click on the links in the drawer to navigate between my pages. However, each time I navigate to a page, all of the widgets on that page get reset to their initial state. I want to ensure that the widgets do not get reset. Another way to think of this is: I only want one instance of each page throughout the lifecycle of the app, instead of creating them new whenever a user navigates to them.
I tried creating a static instance of Page1 that the Drawer uses when the onTap event is fired, but this does not work. Am I thinking about this incorrectly? Do I need to convert to a Stateful widget?
Oh, you're in for a treat... This will be kinda long (sorry) but please read all of it before making decisions and taking action - I promise I am saving you time.
There are many different solutions to this problem, but in general what you're asking about is state management (which is really software engineering, more info here - Understanding state management, and why you never will).
I'll try my best to explain what is happening in your specific case...
Problem:
Think of Navigator as a List of application states, which you can manipulate via its various methods (i.e. pop(), push(), etc.), with this in mind it is clear what is happening - on a button press you're actually removing the current state (page) and right after that you're pushing a new instance of your state (page).
Solution(s):
As I said, there are many solutions to this problem, for example, you may be tempted to store the state (the changes you made to a particular "page") somewhere in a var and inject that var when navigating between "pages", when creating a new instance of that page, but you'll soon run into other problems. This is why I don't think anyone can provide a simple solution to this problem...
First, may I suggest you some useful reads on the matter:
Flutter official docs on state management - When you get to the "Options" section of this, the fun part begins and can quickly get overwhelming, but fear not :P
Be sure to read the medium article mentioned in the start of my answer too, I found it really helpful.
These reads will be more than enough to help you make a decision, plus there are a ton of articles on Medium and YouTube videos touching on the matter of state management with Flutter (even some from the authors of the framework) - just search for "State management with Flutter".
Now my own personal opinion:
If it's a really simple use case and you don't plan to grow (which is almost never the case, trust me), you can just use StatefulWidgets in combination with setState() and maybe InheritedWidget (for dependency injection down the tree, or like React guys call it "lifting state up"). Or instead of the above, maybe have a look at scoped_model, which kinda abstracts all of this for you (tho, I haven't played with it).
What I use right now for a real world project is bloc and flutter_bloc (BLoC = Business Logic Component), I will not get into the details of it, but basically it takes the idea of scoped_model one step further, without over-complicating abstractions. bloc is responsible for abstracting away the "business logic" of your application and flutter_bloc to "inject" the state in your UI and react to state changes (official Flutter position on the matter is that UI = f(State)).
A BLoC has an input and an output, it takes in events as an input (can be user input, or other, any type of event really) and produces a state. In summary that's it about bloc.
A great way to get started is BLoC's official documentation. I highly recommend it. Just go through everything.
(p.s. This may be my personal opinion, but in the end state management in Flutter is all based on some form of using InheritedWidget and setState() in response to user input or other external factors that should change the application state, so I think the BLoC pattern is really on point with abstracting those :P)

Duplicate GlobalKey detected in widget tree

I am running into a globalKey error after I navigate from Screen A to Screen B and click a "Cancel" button to go back to Screen A.
It seems like the issue is that Screen B is either
A) Not being disposed of correctly
B) Is not doing something that it otherwise could
And I don't actually know:
What bad things are happening if I just remove the use of a globalKey? (as to get a better understanding of the fundamentals)
How can I correctly resolve this issue?
StatefulWidget documentation states:enter link description here
A StatefulWidget keeps the same State object when moving from one
location in the tree to another if its creator used a GlobalKey for
its key. Because a widget with a GlobalKey can be used in at most one
location in the tree, a widget that uses a GlobalKey has at most one
associated element. The framework takes advantage of this property
when moving a widget with a global key from one location in the tree
to another by grafting the (unique) subtree associated with that
widget from the old location to the new location (instead of
recreating the subtree at the new location). The State objects
associated with StatefulWidget are grafted along with the rest of the
subtree, which means the State object is reused (instead of being
recreated) in the new location. However, in order to be eligible for
grafting, the widget must be inserted into the new location in the
same animation frame in which it was removed from the old location.
Console Error Output:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown while finalizing the widget tree:
Duplicate GlobalKey detected in widget tree.
The following GlobalKey was specified multiple times in the widget tree. This will lead to parts of
the widget tree being truncated unexpectedly, because the second time a key is seen, the previous
instance is moved to the new location. The key was:
- [LabeledGlobalKey<FormFieldState<String>>#3c76d]
This was determined by noticing that after the widget with the above global key was moved out of its
previous parent, that previous parent never updated during this frame, meaning that it either did
not update at all or updated before the widget was moved, in either case implying that it still
thinks that it should have a child with that global key.
The specific parent that did not update after having one or more children forcibly removed due to
GlobalKey reparenting is:
- Column(direction: vertical, mainAxisAlignment: start, crossAxisAlignment: center, renderObject:
RenderFlex#7595c relayoutBoundary=up1 NEEDS-PAINT)
A GlobalKey can only be specified on one widget at a time in the widget tree.
So this part of the error output:
previous parent never updated during this frame, meaning that it
either did not update at all or updated before the widget was moved
makes me think there was some opportunity for my old Stateful widget to do something (either reposition itself or release something as to be disposed correctly.
This seems to be failing in framework.dart on assert(_children.contains(child)):
#override
void forgetChild(Element child) {
assert(_children.contains(child));
assert(!_forgottenChildren.contains(child));
_forgottenChildren.add(child);
}
In my case, it likes a hot reload bug. Just restart debugging works for me.
Remove the static and final type from the key variable so if
static final GlobalKey<FormState> _abcKey = GlobalKey<FormState>();
change it to
GlobalKey<FormState> _abcKey = GlobalKey<FormState>();
Thanks to Gunter's commments, I determined that this is because the Screens are not being properly disposed.
Flutter's pushReplacement makes a call to Route.dispose which will ultimately dispose the screen.
I am still unsure as to this comes into play:
widget must be inserted into the new location in the same animation
frame
I'm not sure what situation would benefit from such trickery. However, my problem is solved. I just need to make a call to pop or replace.
Here are the available options:
Use push from A to B and just Navigator.pop from B
Use pushReplacement from A to B and from B to A
I've recently started playing with Fluro for routing and there are a few more ways to to handle these situations (Note the optional argument replace):
Use router.navigateTo(context, route, replace: false) from A to B and Navigator.pop from B
Use router.navigateTo(context, route, replace: true) from A to B the same from B to A (the key is replace: true)
Make sure that you don't have a Form parent and a Form child with the same key
I had this issue too.
I had a four screen bottom tabbed application and a 'logout' method.
However, that logout method was calling a pushReplacementNamed.
This prevented the class that held the global keys (different from the logout function) from calling dispose.
The resolution was to change pushReplacementNamed with popAndPushNamed to get back to my 'login' screen.
Best way to solve that, which worked for me:
class _HomeScreenState extends State<HomeScreen> {
GlobalKey<FormState> _homeKey = GlobalKey<FormState>(debugLabel: '_homeScreenkey');
#override
Widget build(BuildContext context) {
return Container(
key: _homeKey,
);
}
}
In my case I wanted to use the static GlobalKey<ScaffoldState> _scaffoldKey but when I used the same widget multiple times it gave this duplicate error.
I wanted to give it a unique string and still use this scaffold state.
So I ended up using:
static GlobalObjectKey<ScaffoldState> _scaffoldKey
and in the initState:
_scaffoldKey = new GlobalObjectKey<ScaffoldState>(id);
Edit:
Actually, silly me. I just simply removed the static and made it GlobalKey again :)
please take SingleChildScrollview:
and after if you use the bloc pettern then use strem with broadcast
code is here:
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/abcd.jpg'),
fit: BoxFit.cover,
),
),
child: Container(child:Form(key: _key,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 100.0, horizontal: 20.0),
child: SingleChildScrollView(child:Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(10.0),
child: Image.asset('assets/images/logo.png', height: 80, width:80,),
),
email(),
Padding(
padding: EdgeInsets.all(5.0),
),
password(),
row(context),
],
),
),
),
),
),
),
resizeToAvoidBottomPadding: false,
);
}
and the bloc pettern code is here:
final _email = StreamController<String>.broadcast();
final _password = StreamController<String>.broadcast();
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password=> _password.stream.transform(validatepassword);
Function(String) get changeEmail=> _email.sink.add;
Function(String) get changePassword => _password.sink.add;
dispose(){
_email.close();
_password.close();
}
}
final bloc=Bloc();
I had similar issue on a StatelessWidget class, Converted it to StatefulWidget and error is gone.
If you have multiple forms with different widgets, you must use separate GlobalKey for each form. Like I have two forms, one with Company signup & one with Profile. So, I declared
GlobalKey<FormState> signUpCompanyKey = GlobalKey<FormState>();
GlobalKey<FormState> signUpProfileKey = GlobalKey<FormState>();
This happened to me, what I did was enclosed the whole view into a navigator using an extension I made
Widget addNavigator() => Navigator(
onGenerateRoute: (_) => MaterialPageRoute(
builder: (context2) => Builder(
builder: (context) => this,
),
),
);
I also got this error. There was a static bloc object in a class and I removed the static keyword which fixed the error.
Events should be added by using the BlocProvider anyway.
I also had a similar error. My answer was that after I updated Flutter some widgets no longer had child or children properties. In my case it was the CircleAvatar. The build doesn't error out initially, but when navigating back and forth through the app it will fail.
*Please review all widgets that require a child then review the updated documentation and make sure you're parameters are still correct.