Please before mark it as duplicate take a look, I have been trying this thread but i can't make it work.
I am trying to make my learning app to display a snackbar, and yes i ran the google cookbook sample and it works, but i am trying to challenge my self doing something else, basically what it is, is just a scaffold with two buttons that increase and decrease, when the index is zero i want to display a snackbar that index is zero, can't decrease more than that. So i got this button and onPressed method calls _onTapPrevious method which passed the context parameter, but i am not sure this is the correct way to pass the context.
new RaisedButton(
padding: const EdgeInsets.all(8.0),
textColor: Colors.white,
color: Colors.redAccent,
onPressed: () => _onTapPrevious(context),
child: new Text("Previous"),
),
Here is the _onTapPrevious method that i created a snackbar, and also checks whether the widget index is > than 0 and if so it can decrease the index, and when it reaches 0 then it should display the snackbar.
_onTapPrevious(BuildContext context) {
final snackBar = SnackBar(content: Text("Index 0, can't decrease more."));
setState(() {
if (widget.index > 0) {
widget.index--;
} else {
Scaffold.of(context).showSnackBar(snackBar);
}
});
}
The error that i get:
flutter: Another exception was thrown: Scaffold.of() called with a context that does not contain a Scaffold.
try to add
final _scaffoldKey = GlobalKey<ScaffoldState>();
to your class, then in Scaffold:
key: _scaffoldKey,
and finally:
_scaffoldKey.currentState.showSnackBar(snackBar);
Hope that helps!
Related
My problem is simple. I was building a ListTile from the list of documents I get from firebase by iterating through the results. The ListTile contains a leading icon, a title, and a trailing favorite IconButton. The list tile shows perfectly as I want it to. But the problem arises when I try to change the color of the IconButton while a user taps on it. For some reason, the code I wrote isn't doing the trick. What i tried to do was to set the value of the IconButton's color by a ternary which uses a class variable named isFavorited. What i wanted it to do is change the color of the IconButton when i tap on that same IconButton. Here is my code block:
// Builds a tile for each brought up names of taxistops
if (retrievedData.isNotEmpty) {
retrievedData.forEach((element) {
if (element.contains(query) ||
element.contains(query.toUpperCase()) ||
element.contains(query.toLowerCase())) {
ListTile listTile = ListTile(
leading: Icon(Icons.local_taxi),
title: Text(element),
trailing: IconButton(
icon: isFavorited
? Icon(
Icons.star,
color: Colors.amber[400],
)
: Icon(Icons.star_border),
onPressed: () => {
setState(() {
isFavorited = true;
}),
addToFavorite()
},
),
onTap: () => close(context, element),
);
searchedTiles.add(listTile);
}
});
}
Any help is appreciated! Thank you in advance!
I think the problem is because you are adding the widget in the list you should preview it directly inside the widget father (ListView) so it can do the setState correctly and not inside a list you created as a data structure to store elements.
I think that's the issue if it still doesn't work I would need to see how you are showing the list.
i'm still new in using flutter driver in testing, but as far as i know there are few identifiers that we can use to locate / identify elements, like By Text, By Type, etc
But the problem is, the app that i want to test doesn't have the identifier that i can use to locate them (please correct me if i'm wrong).. the widget code of the app looks like this
Widget _buildNextButton() {
return Align(
alignment: Alignment.bottomRight,
child: Container(
child: IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => _controller.nextPage(),
),
),
);
}
where that widget is on a class that extends StatefulWidget.
How can i locate that icon in my test script and click it? can i use something like this? And what type of finder should i use? (byValueKey? bySemanticLabel? byType? or what?)
static final arrowKey = find.byValueKey(LoginKey.nextButton);
TestDriverUtil.tap(driver, arrowKey);
We have text and value checks here in Flutter Driver but if you don't have that you can always go the the hierarchy of app.
what I mean by hierarchy is so button has fix or specific parent right?
Let's take your example here, We have Align > Container > IconButton > Icon widget hierarchy which will not be true for others like there might be IconButton but not with the Container parent.
or StreamBuilder or anything that we can think of.
Widget _buildNextButton() {
return Align(
alignment: Alignment.bottomRight,
child: Container(
child: IconButton(
icon: Icon(Icons.arrow_forward),
onPressed: () => print("clicked button"),
),
),
);
}
This hierarchy should be atleast ideal for top bottom or bottom top approach.
Now what I mean by Top to bottom approach is Align must have IconButton and for bottom to up approach we are saying IconButton must have Align widget as parent.
Here i have taken top down approach so what I'm checking from below code is finding IconButton who is decendent of Align Widget.
also i added firstMatchOnly true as I was checking what happens if same hierarchy appears for both so
test('IconButton find and tap test', () async {
var findIconButton = find.descendant(of: find.byType("Align"), matching: find.byType("IconButton"), firstMatchOnly: true);
await driver.waitFor(findIconButton);
await driver.tap(findIconButton);
await Future.delayed(Duration(seconds: 3));
});
to check for multiple IconButtons with same Align as parent, we need to have some difference like parent should be having Text view or other widget.
find.descendant(of: find.ancestor(
of: find.byValue("somevalue"),
matching: find.byType("CustomWidgetClass")), matching: find.byType("IconButton"), firstMatchOnly: true)
usually I go something like above where I have split the code in seperate file and check for that widget.
But ultimately find something unique about that widget and you can work on that.
**In Lib directory dart class for connecting that widget**
class Testing extends StatelessWidget {
Testing();
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: YourClass(), // Next button containing class that need to test
);
}
}
**In Test directory**
testWidgets('Next widget field test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(Testing());
// find Widget
var buttonFind = find.byIcon(Icons.arrow_forward);
expect(buttonFind, findsOneWidget);
IconButton iconButton = tester.firstWidget(buttonFind);
expect(iconButton.color, Colors.blue);
});
I have a simple layout that consists of a PageView.builder widget that contains several pages. The contents of each page are simple, they contain a Container and a Text widget inside of it.
cards is a List of type String and the Pageview.builder widget has an itemCount that's based on the length of this List. The value of the Text in each page is assigned using this List.
List<String> cards = [];
Now, whenever I add a new value to cards List, a variable newPage is used to store the last index position in the List after that element has been added.
After doing this, setState(() {}); is called so that the UI along with the PageView update to reflect the changes made in the List.
The PageView widget does reflect the changes and a new page does get added to it.
However, the problem arises when I try to jump to newly added page right after calling setState.
The error indicates that the index value that jumpToPage is trying to use is out of range in the PageView
cards.add("New card");
newPage = cards.length - 1;
setState(() {});
card_PageController.jumpToPage(newPage);
So, after trying to figure something out, I added a Timer after the setState, so that the framework get's some time to properly update the UI elements.
I'm using a small value of 50 milliseconds in the Timer function and jumping to the new page after the timer gets over.
cards.add("New card");
newPage = cards.length - 1;
setState(() {});
Timer(Duration(milliseconds: 50), () {
card_PageController.jumpToPage(newPage);
})
The addition of a Timer seems to solve the problem and there were no errors after it's addition. However, I'm not sure if this is the right way of tackling this problem.
I'd like to know as to why is this happening, as shouldn't calling jumpToPage directly after setState work without the use of a Timer?
Also, does setState infact take some time to finish updating the UI, even though it isn't async, and that due to this reason the referenced index is invalid? And could this problem have been tackled in a better way?
From the code you shared I can't detect an issue. Especially because I've reproduced what you said you wanted to achieve and it works without issue. Take a look at the code below:
class PageCards extends StatefulWidget {
#override
_PageCardsState createState() => _PageCardsState();
}
class _PageCardsState extends State<PageCards> {
PageController _pageController = PageController();
List<String> cards = [
'page 0',
'page 1',
'page 2',
];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: cards.length,
itemBuilder: (context, index){
return Center(
child: Text(cards[index]),
);
}
),
),
RaisedButton(
child: Text('Add page'),
onPressed: () => addCard(),
),
],
);
}
void addCard(){
setState(() {
cards.add('page ${cards.length}');
});
_pageController.jumpToPage(cards.length - 1);
}
}
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));
After an action (for example, "save"), user is returned to another page in the app.
I want to show a snackbar on the new page to confirm the action, but don't want it to show if the user navigated there without the action. So for example, if user saves "thing", the app sends them to "things" main page and shows the "saved" snackbar, but if they go to "things" main page some other way, I don't want the "saved" snackbar to be there.
Here is my code on the panel that saves, but the destination page does not show the snackbar — regardless where I place the snackbar (before or after navigation), it does not execute the snackbar.
Future<Null> saveThatThing() async {
thing.save();
thing = new Thing();
Navigator.of(context).push(getSavedThingRoute());
Scaffold.of(context).showSnackBar(
new SnackBar(
content: new Text('You saved the thing!'),
),
);
}
What is the best way to do this?
What about if you create key for the screen Scaffold like this (put this object inside the screen state class):
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
and then set this variable to the Scaffold key like this:
return new Scaffold(
key: scaffoldKey,
appBar: new AppBar(
.......,
),
and at the end to show the SnackBar just use somewhere inside the screen state class this code :
scaffoldKey.currentState
.showSnackBar(new SnackBar(content: new Text("Hello")));
or you can wrap the latest code in method like this :
void showInSnackBar(String value) {
scaffoldKey.currentState
.showSnackBar(new SnackBar(content: new Text(value)));}
and then just call this method and pass the snack bar message inside.
If you want to show the snack bar message on the next screen, after you navigate away from the initial context, just call the snack bar method on the next screen, not on the first screen.
Hope this will help
You have to return a result to the previous page which will represent the action shown on the page.
In the 1st page When you are navigating to the page change a few things.
bool result=await Navigator.of(context).push(/*Wherever you want*/);
Then in the second page when you are returning to the previous page send some result to the 1st page.
Navigator.of(context).pop(true);
if the work is not success you can return false.
You can return any type of object as result
Then In the 1st page you can check for the result and show the snackBar accordingly
bool result=await Navigator.of(context).push(/*Wherever you want*/);
if(result!=null && result==true){
//Show SnackBar
}else{
//Some other action if your work is not done
}
Since showing SackBar require an active Scaffold, you will need to pass the message to the new route page something like getSavedThingRoute(message: 'You saved the thing!') and the new page is responsible for displaying the message.
Typically I happen to use Navigation.pop({'message': 'You saved the thing!', 'error': false}) to pass the message.
In your 2nd Page's class, override the initState method
#override
void initState() {
super.initState();
// Shows the SnackBar as soon as this page is opened.
Future(() {
final snackBar = SnackBar(content: Text('Hello World'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}
In case anyone using Flushbar plugin. Put this Inside your build function before return statement.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Flushbar(
message: "Message from the top",
flushbarPosition: FlushbarPosition.TOP,
icon: Icon(
Icons.info_outline,
size: 28.0,
color: Color(0xff00d3fe),
),
flushbarStyle: FlushbarStyle.FLOATING,
duration: Duration(seconds: 5),
)..show(_scaffoldKey.currentState.context);
});}
of course don't forget the scaffold key
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
In my case the reason for the snackbar not being shown was the floating action button , I had a logic to put the fab or not and when there was no fab the snack bar did not show , I did not find a proper solution for this but putting a fab like this solved my problem .
however putting Container() , null will cause problems.
Visibility(
visible: false,
child: FloatingActionButton(
onPressed: () {},
),
);