Flutter - Update ListView and reset scroll position - dart

I have a ListView, where i change the Items in the List by clicking a button. Problem now is, that when i scroll down to the end of the list then click the Button, the Items change but the position of the list is still the same (so when i scrolled down and item #2 of my previous list was at the top, then item #2 of the new list will also be at the top).
What i want is that the List starts at the top again and i have to scroll down again.
my main.dart:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List _list;
List _list1 = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'];
List _list2 = ['Element A', 'Element B', 'Element C', 'Element D', 'Element E', 'Element F'];
bool _isList1;
#override
void initState() {
_list = _list1;
_isList1 = true;
super.initState();
}
void _changeList() {
setState(() {
if (_isList1) {
_list = _list2;
_isList1 = false;
} else {
_list = _list1;
_isList1 = true;
}
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Schürer',
home: Scaffold(
appBar: AppBar(
title: Text('ListView'),
),
body: Column(
children: <Widget>[
RaisedButton(
child: Text("Change List"),
onPressed: _changeList,
),
Expanded(
child: ListView(
children: _list
.map(
(item) => Card(
child: Container(
padding: EdgeInsets.all(50),
color: Colors.black26,
child: Text(item),
),
),
)
.toList(),
),
),
],
),
));
}
}

here you have to provide key to the ListView to differentiate two different listview. if you do not provide key then flutter think that both are same list view and it keep tracking listview using index of it.
what could be solution in your case if we consider first element of list as a key.
....
child: ListView(
key: ObjectKey(_list[0]),
children: _list
.....

This technique worked for me:
create the ScrollController variable:
ScrollController _scrollController = ScrollController();
...
...
_scrollController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
This is the description of animateTo() by flutter:
Animates the position from its current value to the given value.
I called this function whenever the textfield value is changed.

With a Scroll Controller you can do the following which will take you to the top of your ListView:
ScrollController _scrollController = ScrollController();
...
...
_scrollController.jumpTo(0);
Of course, you can change the index from 0 to any ListTile index, to jump to whichever ListTile you wish in your ListView.

Related

Using tabbar with animated list results in duplicate global key error

I am trying to implement Flutter's Tab Bar with 3 tabs and an AnimatedList inside those tabs. I want to use the same list and filter the list according to each tab (past tasks, today's tasks, and future tasks), however during my implementation of the tab bar together with the animatedlist I am getting an error regarding a duplicate global key in the widget tree. https://pastebin.com/iAW6DH9m . What would be the best way to deal with this error? Thank you for any help.
edit: I tried using this method to fix this. Multiple widgets used the same GlobalKey while it did fix my error I was then unable to access "currentstate" method on the key to be able to add more items to the list. I then tried a similar method using using GlobalKey and it resulted in a similar error of duplicate global keys.
This is my tab bar implementation
import 'package:flutter/material.dart';
import 'search_widget.dart';
import 'animatedlist_widget.dart';
class Dashboard extends StatefulWidget {
#override
_DashboardState createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
centerTitle: true,
actions: <Widget>[
new IconButton(icon: new Icon(Icons.grid_on), onPressed: null)
],
title: new Text('Dashboard'),
elevation: 0,
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
_onFabPress(context);
},
child: new Icon(Icons.add)),
body: Scaffold(
appBar: new SearchWidget(
onPressed: () => print('implement search'),
icon: Icons.search,
title: 'Search',
preferredSize: Size.fromHeight(50.0),
),
body: DefaultTabController(
length: 3,
child: Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: Container(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: new TabBar(
unselectedLabelColor: Colors.black45,
labelColor: Colors.white,
indicator: CustomTabIndicator(),
tabs: <Widget>[
new Tab(text: "Past"),
new Tab(text: "Today"),
new Tab(text: "Future")
]),
),
),
),
body: new TabBarView(
children: <Widget>[
AnimatedTaskList(),
AnimatedTaskList(),
AnimatedTaskList()
],
)
),
),
),
);
}
void _onFabPress(context) {
AnimatedTaskList().addUser();
}
/*showModalBottomSheet(
context: context,
builder: (BuildContext bc) {
return Container(
child: new Wrap(children: <Widget>[
new TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter Task Title')),
new TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter Task Details',
)),
]));
});
}*/
}
class CustomTabIndicator extends Decoration {
#override
BoxPainter createBoxPainter([onChanged]) {
// TODO: implement createBoxPainter
return new _CustomPainter(this, onChanged);
}
}
class _CustomPainter extends BoxPainter {
final CustomTabIndicator decoration;
_CustomPainter(this.decoration, VoidCallback onChanged)
: assert(decoration != null),
super(onChanged);
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
// TODO: implement paint
assert(configuration != null);
assert(configuration.size != null);
final indicatorHeight = 30.0;
final Rect rect = Offset(
offset.dx, (configuration.size.height / 2) - indicatorHeight / 2) &
Size(configuration.size.width, indicatorHeight);
final Paint paint = Paint();
paint.color = Colors.blueAccent;
paint.style = PaintingStyle.fill;
canvas.drawRRect(RRect.fromRectAndRadius(rect, Radius.circular(30)), paint);
}
}
This is my animatedlist class:
import 'package:flutter/material.dart';
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
class AnimatedTaskList extends StatefulWidget {
void addUser() {
int index = listData.length;
listData.add(
TaskModel(
taskTitle: "Grocery Shopping",
taskDetails: "Costco",
),
);
_listKey.currentState
.insertItem(index, duration: Duration(milliseconds: 500));
}
#override
_AnimatedTaskListState createState() => _AnimatedTaskListState();
}
class _AnimatedTaskListState extends State<AnimatedTaskList> {
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: SafeArea(
child: AnimatedList(
key: _listKey,
initialItemCount: listData.length,
itemBuilder:
(BuildContext context, int index, Animation animation) {
return Card(
child: FadeTransition(
opacity: animation,
child: ListTile(
title: Text(listData[index].taskTitle),
subtitle: Text(listData[index].taskDetails),
onLongPress: () {
//todo delete user
},
)));
})),
);
}
}
class TaskModel {
TaskModel({this.taskTitle, this.taskDetails});
String taskTitle;
String taskDetails;
}
List<TaskModel> listData = [
TaskModel(
taskTitle: "Linear Algebra",
taskDetails: "Chapter 4",
),
TaskModel(
taskTitle: "Physics",
taskDetails: "Chapter 9",
),
TaskModel(
taskTitle: "Software Construction",
taskDetails: "Architecture",
),
];
I fixed my issue by moving
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
into my _AnimatedTaskListState class, and adding a constructor and private key to my AnimatedTaskList class
final GlobalKey<AnimatedListState> _key;
AnimatedTaskList(this._key);
#override
_AnimatedTaskListState createState() => _AnimatedTaskListState(_key);
then in my tab bar implementation I changed it to reflect my new constructor
AnimatedTaskList(GlobalKey<AnimatedListState>(debugLabel: "key 1"));
AnimatedTaskList(GlobalKey<AnimatedListState>(debugLabel: "key 2"));
AnimatedTaskList(GlobalKey<AnimatedListState>(debugLabel: "key 3"));

Flutter - PopupMenu on long press

I'm making an image gallery and I need the user to be able to long-press an image to show a popup menu which will let him delete the image.
My code, so far:
return GestureDetector(
onLongPress: () {
showMenu(
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: Image.memory(
this._asset.thumbData.buffer.asUint8List(),
fit: BoxFit.cover,
gaplessPlayback: true,
),
);
Which produces:
But also, I couldn't find out how to completely remove the image's widget when the longPress function is called. How to do so?
The OP and the First Answerer bypassed the original problem using PopupMenuButton, which worked fine in their case. But I think the more general question of how to position one's own menu and how to receive the user's response without using PopupMenuButton is worth answering, because sometimes we want a popup menu on a custom widget, and we want it to appear on some gestures other than a simple tap (e.g. the OP's original intention was to long-press).
I set out to make a simple app demonstrating the following:
Use a GestureDetector to capture long-press
Use the function showMenu() to display a popup menu, and position it near the finger's touch
How to receive the user's selection
(Bonus) How to make a PopupMenuEntry that represents multiple values (the oft-used PopupMenuItem can only represent a single value)
The result is, when you long-press on a big yellow area, a popup menu appears on which you can select +1 or -1, and the big number would increment or decrement accordingly:
Skip to the end for the entire body of code. Comments are sprinkled in there to explain what I am doing. Here are a few things to note:
showMenu()'s position parameter takes some effort to understand. It's a RelativeRect, which represents how a smaller rect is positioned inside a bigger rect. In our case, the bigger rect is the entire screen, the smaller rect is the area of touch. Flutter positions the popup menu according to these rules (in plain English):
if the smaller rect leans toward the left half of the bigger rect, the popup menu would align with the smaller rect's left edge
if the smaller rect leans toward the right half of the bigger rect, the popup menu would align with the smaller rect's right edge
if the smaller rect is in the middle, which edge wins depends on the language's text direction. Left edge wins if using English and other left-to-right languages, right edge wins otherwise.
It's always useful to reference PopupMenuButton's official implementation to see how it uses showMenu() to display the menu.
showMenu() returns a Future. Use Future.then() to register a callback to handle user selection. Another option is to use await.
Remember that PopupMenuEntry is a (subclass of) StatefulWidget. You can layout any number of sub-widgets inside it. This is how you represent multiple values in a PopupMenuEntry. If you want it to represent two values, just make it contain two buttons, however you want to lay them out.
To close the popup menu, use Navigator.pop(). Flutter treats popup menus like a smaller "page". When we display a popup menu, we are actually pushing a "page" to the navigator's stack. To close a popup menu, we pop it from the stack, thus completing the aforementioned Future.
Here is the full code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _count = 0;
var _tapPosition;
void _showCustomMenu() {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
position: RelativeRect.fromRect(
_tapPosition & const Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
)
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: _storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(
fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
If you are going to use a gridView or listview for laying out the images on the screen, you can wrap each item with a gesture detector then you should keep your images in a list somewhere, then simply remove the image from the list and call setState().
Something like the following. (This code will probably won't compile but it should give you the idea)
ListView.builder(
itemCount: imageList.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onLongPress: () {
showMenu(
onSelected: () => setState(() => imageList.remove(index))}
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: imageList[index],
);
}
)
Edit: You can use a popup menu too, like following
Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
width: 100,
child: PopupMenuButton(
child: FlutterLogo(),
itemBuilder: (context) {
return <PopupMenuItem>[new PopupMenuItem(child: Text('Delete'))];
},
),
),
Building on the answers by Nick Lee and hacker1024, but instead of turning the solution into a mixin, you could simply just turn it into a widget:
class PopupMenuContainer<T> extends StatefulWidget {
final Widget child;
final List<PopupMenuEntry<T>> items;
final void Function(T) onItemSelected;
PopupMenuContainer({#required this.child, #required this.items, #required this.onItemSelected, Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => PopupMenuContainerState<T>();
}
class PopupMenuContainerState<T> extends State<PopupMenuContainer<T>>{
Offset _tapDownPosition;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details){
_tapDownPosition = details.globalPosition;
},
onLongPress: () async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
T value = await showMenu<T>(
context: context,
items: widget.items,
position: RelativeRect.fromLTRB(
_tapDownPosition.dx,
_tapDownPosition.dy,
overlay.size.width - _tapDownPosition.dx,
overlay.size.height - _tapDownPosition.dy,
),
);
widget.onItemSelected(value);
},
child: widget.child
);
}
}
And then you'd use it like this:
child: PopupMenuContainer<String>(
child: Image.asset('assets/image.png'),
items: [
PopupMenuItem(value: 'delete', child: Text('Delete'))
],
onItemSelected: (value) async {
if( value == 'delete' ){
await showDialog(context: context, child: AlertDialog(
title: Text('Delete image'),
content: Text('Are you sure you want to delete the image?'),
actions: [
uiFlatButton(child: Text('NO'), onTap: (){ Navigator.of(context).pop(false); }),
uiFlatButton(child: Text('YES'), onTap: (){ Navigator.of(context).pop(true); }),
],
));
}
},
),
Adjust the code to fit your needs.
Nick Lee's answer can be turned into a mixin quite easily, which can then be used anywhere you want to use a popup menu.
The mixin:
import 'package:flutter/material.dart' hide showMenu;
import 'package:flutter/material.dart' as material show showMenu;
/// A mixin to provide convenience methods to record a tap position and show a popup menu.
mixin CustomPopupMenu<T extends StatefulWidget> on State<T> {
Offset _tapPosition;
/// Pass this method to an onTapDown parameter to record the tap position.
void storePosition(TapDownDetails details) => _tapPosition = details.globalPosition;
/// Use this method to show the menu.
Future<T> showMenu<T>({
#required BuildContext context,
#required List<PopupMenuEntry<T>> items,
T initialValue,
double elevation,
String semanticLabel,
ShapeBorder shape,
Color color,
bool captureInheritedThemes = true,
bool useRootNavigator = false,
}) {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
return material.showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
_tapPosition.dx,
_tapPosition.dy,
overlay.size.width - _tapPosition.dx,
overlay.size.height - _tapPosition.dy,
),
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
shape: shape,
color: color,
captureInheritedThemes: captureInheritedThemes,
useRootNavigator: useRootNavigator,
);
}
}
And then, to use it:
import 'package:flutter/material.dart';
import './custom_context_menu.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with CustomPopupMenu {
var _count = 0;
void _showCustomMenu() {
this.showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
Answer for 2023
In Flutter 3.7 there is now a ContextMenuRegion widget that you can wrap around any existing widget. When the user long presses or right-clicks (depending on the platform), the menu you give it will appear.
return Scaffold(
body: Center(
child: ContextMenuRegion(
contextMenuBuilder: (context, offset) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors(
primaryAnchor: offset,
),
buttonItems: <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {
ContextMenuController.removeAny();
},
label: 'Save',
),
],
);
},
child: const SizedBox(
width: 200.0,
height: 200.0,
child: FlutterLogo(),
),
),
),
);

Flutter Access parent Scaffold from different dart file

I have this:
final GlobalKey<ScaffoldState> _scaffoldkey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
key: _scaffoldkey,
drawer: Menu(),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
...
],
),
),
body: TabBarView(
children: <Widget>[
...
],
),
),
);
}
}
Now, the drawer: Menu() is imported from another menu.dart file, which looks like this:
class Menu extends StatelessWidget {
final GlobalKey<ScaffoldState> drawerKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return new Drawer(
key: drawerKey,
child: new ListView(
children: <Widget>[
new ListTile(
dense: true,
title: new Text('My Text'),
onTap: () {
// On tap this, I want to show a snackbar.
scaffoldKey.currentState.showSnackBar(showSnack('Error. Could not log out'));
},
),
],
),
);
}
}
With the above approach, I get
NoSuchMethodError: The method 'showSnackBar' was called on null.
An easy solution is to tuck the entire menu.dart contents in the drawer: ... directly.
Another way I'm looking at is being able to reference the parent scaffold in order to display the snackbar.
How can one achieve that?
Why can't one even just call the snackbar from anywhere in Flutter and compulsorily it has to be done via the Scaffold? Just why?
You should try to avoid using GlobalKey as much as possible; you're almost always better off using Scaffold.of to get the ScaffoldState. Since your menu is below the scaffold in the widget tree, Scaffold.of(context) will do what you want.
The reason what you're attempting to do doesn't work is that you are creating two seperate GlobalKeys - each of which is its own object. Think of them as global pointers - since you're creating two different ones, they point to different things. And the state should really be failing analysis since you're passing the wrong type into your Drawer's key field...
If you absolutely have to use GlobalKeys for some reason, you would be better off passing the instance created in your outer widget into your Menu class as a member i.e. this.scaffoldKey, but this isn't recommended.
Using Scaffold.of, this is what your code would look like in the onTap function:
onTap: () {
// On tap this, I want to show a snackbar.
Scaffold.of(context).showSnackBar(showSnack('Error. Could not log out'));
},
You can achieve this functionality by using builder widget you don't need to make separate GlobalKey or pass key as a parameter. Just wrap a widget to Builder widget
class CustomDrawer extends StatelessWidget {#override Widget build(BuildContext context) {
return new Drawer(
child: new ListView(
children: <Widget>[
new Builder(builder: (BuildContext innerContext) {
return ListTile(
dense: true,
title: new Text('My Text'),
onTap: () {
Navigator.of(context).pop();
Scaffold.of(innerContext).showSnackBar(SnackBar(
content: Text('Added added into cart'),
duration: Duration(seconds: 2),
action: SnackBarAction(label: 'UNDO', onPressed: () {}),
));
}
);
})
],
),
);}}
From your first question
In other to reference the parent scaffold in the menu widget you can pass the _scaffoldkey to the menu widget as parameter and use ScaffoldMessenger.of() to show snackbar as shown below
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// Root Widget
#override
Widget build(BuildContext context) {
return MaterialApp(
// App name
title: 'Flutter SnackBar',
// Theme
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(title: 'SnackBar'),
);
}
}
class Test extends StatefulWidget {
final String? title;
final GlobalKey<ScaffoldState> _scaffoldkey = new GlobalKey<ScaffoldState>();
Test({#required this.title});
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
key: widget._scaffoldkey,
drawer: Menu(parentScaffoldkey:widget._scaffoldkey),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
Tab(text:"Home"),
Tab(text:"About")
],
),
),
body: TabBarView(
children: <Widget>[
Text("Home"),
Text("About")
],
),
),
);
}
}
Menu part as shown
class Menu extends StatelessWidget {
final parentScaffoldkey;
Menu({this.parentScaffoldkey});
#override
Widget build(BuildContext context) {
return new Drawer(
child: new ListView(
children: <Widget>[
new ListTile(
dense: true,
title: new Text('My Text'),
onTap: () {
// On tap show a snackbar.
// ScaffoldMessenger will call the nearest Scaffold to show snackbar
ScaffoldMessenger.of(this.parentScaffoldkey.currentContext).showSnackBar(SnackBar(content:Text('Error. Could not log out')));
},
),
],
),
);
}
}
Also,you have to call snackbar via Scaffold because it provides the SnackBar API and manages it

Overflowing parent widgets

I'm trying to create a widget that has a button and whenever that button is pressed, a list opens up underneath it filling in all of the space under the button. I implemented it with a simple Column, something like this:
class _MyCoolWidgetState extends State<MyCoolWidget> {
...
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new MyButton(...),
isPressed ? new Expanded(
child: new SizedBox(
width: MediaQuery.of(context).size.width,
child: new MyList()
)
) : new Container()
]
)
}
}
This works totally fine in a lot of cases, but not all.
The problem I'm having with creating this widget is that if a MyCoolWidget is placed inside a Row for example with other widgets, lets say other MyCoolWidgets, the list is constrained by the width that the Row implies on it.
I tried fixing this with an OverflowBox, but with no luck unfortunately.
This widget is different from tabs in the sense that they can be placed anywhere in the widget tree and when the button is pressed, the list will fill up all the space under the button even if this means neglecting constraints.
The following image is a representation of what I'm trying to achieve in which "BUTTON1" and "BUTTON2" or both MyCoolWidgets in a Row:
Edit: Snippet of the actual code
class _MyCoolWidgetState extends State<MyCoolWidget> {
bool isTapped = false;
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new SizedBox(
height: 20.0,
width: 55.0,
child: new Material(
color: Colors.red,
child: new InkWell(
onTap: () => setState(() => isTapped = !isTapped),
child: new Text("Surprise"),
),
),
),
bottomList()
],
);
}
Widget comboList() {
if (isTapped) {
return new Expanded(
child: new OverflowBox(
child: new Container(
color: Colors.orange,
width: MediaQuery.of(context).size.width,
child: new ListView( // Random list
children: <Widget>[
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
new Text("ok"),
],
)
)
),
);
} else {
return new Container();
}
}
}
I'm using it as follows:
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new Expanded(child: new MyCoolWidget()),
new Expanded(child: new MyCoolWidget()),
]
)
}
}
Here is a screenshot of what the code is actually doing:
From the comments, it was clarified that what the OP wants is this:
Making a popup that covers everything and goes from wherever the button is on the screen to the bottom of the screen, while also filling it horizontally, regardless of where the button is on the screen. It would also toggle open/closed when the button is pressed.
There are a few options for how this could be done; the most basic would be to use a Dialog & showDialog, except that it has some issues around SafeArea that make that difficult. Also, the OP is asking for the button to toggle rather than pressing anywhere not the dialog (which is what dialog does - either that or blocks touches behind the dialog).
This is a working example of how to do something like this. Full disclaimer - I'm not stating that this is a good thing to do, or even a good way to do it... but it is a way to do it.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
// We're extending PopupRoute as it (and ModalRoute) do a lot of things
// that we don't want to have to re-create. Unfortunately ModalRoute also
// adds a modal barrier which we don't want, so we have to do a slightly messy
// workaround for that. And this has a few properties we don't really care about.
class NoBarrierPopupRoute<T> extends PopupRoute<T> {
NoBarrierPopupRoute({#required this.builder});
final WidgetBuilder builder;
#override
Color barrierColor;
#override
bool barrierDismissible = true;
#override
String barrierLabel;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return new Builder(builder: builder);
}
#override
Duration get transitionDuration => const Duration(milliseconds: 100);
#override
Iterable<OverlayEntry> createOverlayEntries() sync* {
// modalRoute creates two overlays - the modal barrier, then the
// actual one we want that displays our page. We simply don't
// return the modal barrier.
// Note that if you want a tap anywhere that isn't the dialog (list)
// to close it, then you could delete this override.
yield super.createOverlayEntries().last;
}
#override
Widget buildTransitions(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
// if you don't want a transition, remove this and set transitionDuration to 0.
return new FadeTransition(opacity: new CurvedAnimation(parent: animation, curve: Curves.easeOut), child: child);
}
}
class PopupButton extends StatefulWidget {
final String text;
final WidgetBuilder popupBuilder;
PopupButton({#required this.text, #required this.popupBuilder});
#override
State<StatefulWidget> createState() => PopupButtonState();
}
class PopupButtonState extends State<PopupButton> {
bool _active = false;
#override
Widget build(BuildContext context) {
return new FlatButton(
onPressed: () {
if (_active) {
Navigator.of(context).pop();
} else {
RenderBox renderbox = context.findRenderObject();
Offset globalCoord = renderbox.localToGlobal(new Offset(0.0, context.size.height));
setState(() => _active = true);
Navigator
.of(context, rootNavigator: true)
.push(
new NoBarrierPopupRoute(
builder: (context) => new Padding(
padding: new EdgeInsets.only(top: globalCoord.dy),
child: new Builder(builder: widget.popupBuilder),
),
),
)
.then((val) => setState(() => _active = false));
}
},
child: new Text(widget.text),
);
}
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new SafeArea(
child: new Container(
color: Colors.white,
child: new Column(children: [
new PopupButton(
text: "one",
popupBuilder: (context) => new Container(
color: Colors.blue,
),
),
new PopupButton(
text: "two",
popupBuilder: (context) => new Container(color: Colors.red),
)
]),
),
),
);
}
}
For even more outlandish suggestions, you can take the finding the location part of this and look at this answer which describes how to create a child that isn't constrained by it's parent's position.
However you end up doing this, it's probably best that the list not to be a direct child of the button as a lot of things in flutter depend on a child's sizing and making it be able to expand to the full screen size could quite easily cause problems.

How do I use radio buttons inside popup menus?

I'm trying to create a popup menu that contains selectable radio buttons in order to change a view type (e.g. gallery, cards, swipe, grid, list, etc.).
The issue I'm running into is that PopupMenu has its own callbacks for selecting values, and so does Radio and RadioListTile.
Ignore RadioListTile's onChanged
Here's my first attempt. This actually works, except that the buttons are perpetually grayed out. Giving the RadioListTiles a non-null noop function results in the buttons no longer grayed out (disabled), but then the popup menu no longer works.
new PopupMenuButton<String>(
...
itemBuilder: (ctx) => <PopupMenuEntry<String>>[
new PopupMenuItem(
child: new RadioListTile(
title: new Text("Cards"),
value: 'cards',
groupValue: _view,
onChanged: null),
value: 'cards'),
new PopupMenuItem(
child: new RadioListTile(
title: new Text("Swipe"),
value: 'swipe',
groupValue: _view,
onChanged: null),
value: 'swipe'),
],
onSelected: (String viewType) {
_view = viewType;
}));
Use RadioListTile, ignore PopupMenu
Second attempt is to ignore the PopupMenu entirely and just use RadioListTile onChanged. The buttons are not grayed-out/disabled, but are also not functional.
new PopupMenuButton<String>(
...
itemBuilder: (ctx) => <PopupMenuEntry<Null>>[
new PopupMenuItem(
child: new RadioListTile(
title: new Text("Cards"),
value: 'cards',
groupValue: _view,
onChanged: (v) => setState(() => _view = v)),
value: 'cards'),
new PopupMenuItem(
child: new RadioListTile(
title: new Text("Swipe"),
value: 'swipe',
groupValue: _view,
onChanged: (v) => setState(() => _view = v)),
value: 'swipe'),
],
));
What's the correct approach? PopupMenu works well with extremely simple menus, but the element selection is giving me conflicts. Is there a way to get a "dumb" popup menu that displays a column of widgets (styled like a menu) at the button?
I think the best solution for you would be to use CheckedPopupMenuItems instead of a radio list. The functionality should be exactly what you want to achieve, isn't it?
Here is a small example:
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _selectedView = 'Card';
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('TestProject'),
actions: <Widget>[
new PopupMenuButton(
onSelected: (value) => setState(() => _selectedView = value),
itemBuilder: (_) => [
new CheckedPopupMenuItem(
checked: _selectedView == 'Card',
value: 'Card',
child: new Text('Card'),
),
new CheckedPopupMenuItem(
checked: _selectedView == 'Swipe',
value: 'Swipe',
child: new Text('Swipe'),
),
new CheckedPopupMenuItem(
checked: _selectedView == 'List',
value: 'List',
child: new Text('List'),
),
],
),
],
),
body: new Center(child: new Text(_selectedView)),
);
}
}
The problem is that the PopupMenuButton is maintaining the popup dialog as private state (it even pushes a new route onto the Navigator stack). Calling setState won't rebuild the items. You can use an AnimatedBuilder and a ValueNotifier to get around this.
Here's an example of a working radio button list inside a popup:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
State createState() => new MyHomePageState();
}
enum Fruit {
apple,
banana,
}
class MyHomePageState extends State<MyHomePage> {
ValueNotifier<Fruit> _selectedItem = new ValueNotifier<Fruit>(Fruit.apple);
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new PopupMenuButton<Fruit>(
itemBuilder: (BuildContext context) {
return new List<PopupMenuEntry<Fruit>>.generate(
Fruit.values.length,
(int index) {
return new PopupMenuItem(
value: Fruit.values[index],
child: new AnimatedBuilder(
child: new Text(Fruit.values[index].toString()),
animation: _selectedItem,
builder: (BuildContext context, Widget child) {
return new RadioListTile<Fruit>(
value: Fruit.values[index],
groupValue: _selectedItem.value,
title: child,
onChanged: (Fruit value) {
_selectedItem.value = value;
},
);
},
),
);
},
);
},
),
),
);
}
}

Resources