What I am trying to do in my app is to add a dropdown based on the list contents. I have something like this:
[
{
id: val,
displayName: Enter value,
type: string,
value: "any"
},
{
id: si,
displayName: Source,
type: list,
value: [
MO
],
data: [
{id: 1, displayId: MO},
{id: 2, displayId: AO},
{id: 3, displayId: OffNet}
]
}
]
Currently there are 2 entries. What I want to do is display a dropdown containing those options (Enter value and Source) as 2 entries of dropdown:
If Enter value is selected a text box next to it should be displayed, since it has a type of string.
If Source option in dropdown is selected another dropdown containing those entries (MO, AO, Offnet) should be present as a dropdown value, since it has a type of list.
In short, based on the selection of the 1st dropdown a widget to be displayed (either text box or another dropdown) should be chosen.
If anyone knows or previously had done the same please help me with this, Thanks.
I'd make use of StatefulWidget to achieve what you need (if you're not using more advanced state management options). State would be helpful to track user's choices, as well as to decide whether to render a text field or another dropdown (or nothing at all).
I've added a complete working example below. Note that it does not follow best practices in a sense that you would probably want to split it up in separate small widgets for better composability (and readability). However, I've opted for quick-and-dirty approach to fit everything in one place.
Also note that you'd probably want to do some more processing once a user makes a choice. Here, I simply illustrate how to render different widgets based on a user's choice (or more generally, changes in StatefulWidget's state). Hence, this example is used to highlight one principle only.
import 'package:flutter/material.dart';
void main() {
runApp(DropdownExample());
}
class DropdownExample extends StatefulWidget {
#override
_DropdownExampleState createState() => _DropdownExampleState();
}
class _DropdownExampleState extends State<DropdownExample> {
String type;
int optionId;
final items = [
{
"displayName": "Enter value",
"type": "string",
},
{
"displayName": "Source",
"type": "list",
"data": [
{"id": 1, "displayId": "MO"},
{"id": 2, "displayId": "AO"},
{"id": 3, "displayId": "OffNet"}
]
}
];
#override
Widget build(BuildContext context) {
Widget supporting = buildSupportingWidget();
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Dropdown Example")),
body: Center(
child: Container(
height: 600,
width: 300,
child: Row(
children: <Widget>[
buildMainDropdown(),
if (supporting != null) supporting,
],
),
),
),
),
);
}
Expanded buildMainDropdown() {
return Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton(
value: type,
hint: Text("Select a type"),
items: items
.map((json) => DropdownMenuItem(
child: Text(json["displayName"]), value: json["type"]))
.toList(),
onChanged: (newType) {
setState(() {
type = newType;
});
},
),
),
);
}
Widget buildSupportingWidget() {
if (type == "list") {
List<Map<String, Object>> options = items[1]["data"];
return Expanded(
child: DropdownButtonHideUnderline(
child: DropdownButton(
value: optionId,
hint: Text("Select an entry"),
items: options
.map((option) => DropdownMenuItem(
child: Text(option["displayId"]), value: option["id"]))
.toList(),
onChanged: (newId) => setState(() {
this.optionId = newId;
}),
),
),
);
} else if (type == "string") {
return Expanded(child: TextFormField());
}
return null;
}
Related
I want to show json data with DataTable() but i get error:
type '(dynamic) => Column?' is not a subtype of type '(dynamic) => DataTable' of 'f'
My question is how i can resolve the error but most importantly how can i iterate through the list block_data and then for each map show header and title in seperate DataTable() where they are on top of eachother and eachdescription below each DataTable()?
This is the view where i call the method setTestData() which awaits the Future AppContent().getAppContent() and then set the data to the field testObject and i also initialize setTestData() in current state. I can then use testObject to acces the json data.
My goal is to show EACH map from list block_data as SEPERATE DataTable for my usecase i have to do that. The reason why i want to this like that is because i also want to show the description below DataTable as a seperate Text() widget because it can be too long and in my usecase it has to be below the table
I now have this AppView statefull widget which i want to use to show each DataTable() seperatly based on each map from list block_data. I am not sure if i do it the right way but right now i get the error so it is more unclear if i can even achieve my goal this way:
class AppView extends StatefulWidget {
const AppView({Key? key}) : super(key: key);
#override
_AppViewState createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
late Map<String, dynamic> testObject = {};
setTestData() async {
await AppContent()
.getAppContent()
.then((result) => setState(() => testObject = result));
}
#override
void initState() {
setTestData();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: IconButton(
icon: const Icon(Icons.menu,
size: 40), // change this size and style
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return Column(
children: [
FutureBuilder(
future: AppContent().getAppContent(),
builder: (context, snapshot) {
if (snapshot.hasData) {
debugPrint(testObject["data"]["views"]["books"][0]
["block_data"]
.toString());
return Column(
children: [
Container(
alignment: Alignment.topLeft,
child: (testObject["data"]["views"]["books"]
[0]["block_data"])
.map<DataTable>((object) {
if (object.containsKey("header")) {
return Column(
children: [
DataTable(
horizontalMargin: 0,
columnSpacing: 75,
columns: <DataColumn>[
DataColumn(
label: Container(
padding: EdgeInsets.zero,
child: Text(
object["header"]
.toString(),
)),
),
],
rows: <DataRow>[
DataRow(
cells: <DataCell>[
DataCell(Text(
object['title']
.toString(),
DataCell(Text(object['date'].toString()
))
],
),
],
),
Text(object["description"]
.toString())
],
);
} else {
Container();
}
}).toList()),
Text(
"This is another placeholder for another list")
],
);
} else {
return CircularProgressIndicator();
}
}),
],
);
}, childCount: 1),
),
],
),
),
);
}
}
This is the method AppContent().getAppContent() which grabs the json:
class AppContent {
Future<Map<String, dynamic>> getAppContent() async {
String jsonData = await rootBundle.loadString('assets/test.json');
Map<String, dynamic> data = jsonDecode(jsonData);
return data;
}
}
And this the json which i call:
{
"data": {
"views": {
"books": [
{
"block_type": "list",
"block_data": [
{
"header": "FAQ",
"long_text_type": "description",
"title": "Service fees",
"date": "19-01-2022",
"description": "Information about fees and surcharges."
},
{
"header": "FAQ",
"long_text_type": "description",
"title": "Returns & Refunds",
"date": "03-06-2022",
"description": "How to return products and recieve refunds.."
}
]
}
}
}
}
Edit
I want it to look like the picture below where i have a datable for and description below that. But i want it for each map in list block_data and so i want to show the next map below the description and then show basicly DataTable -> description -> DataTable -> description. But i want to iterate through list block_data and generate DataTable and Text based on maps inside list which could be more than just two maps
There are multiple issues here.
Your JSON has an error (probably a copy/paste error, but double-check it).
Instead of
"block_data": "block_data": [ you should have "block_data": [.
Moreover, a bracket is missing at the end of your JSON file (but again, I guess that it's because you only showed a part of your file to help us investigate your problem)
The error you wrote in your question is related to your .map<DataTable>((object) {
When using the .map, the Object you specify is the return type of your mapping. In your case you're returning a Column and not a DataTable.
If you want to iterate on a list, and create a list of Widgets in return, you can use this instead:
.map<Widget>((object) {
Finally, and the most important point of this answer : you're having problems here because you're not converting your JSON file in a Dart Object you can easily manipulate.
You can simply paste your JSON file on this website : https://javiercbk.github.io/json_to_dart/ and retrieve the code to add to your project.
Then, you'll have a model with a fromJson and a toJson methods.
Thanks to those methods, you'll be able to create Dart Objects from your JSON values, and thus to create your Widgets easily.
With this answer, you should be good to go. Add a comment if I need to add more details.
I am working on creating a hashMap for my flutter program and would like some input on it. As I created my hashMap in another dart file that is not the main dart file and I have no idea on how to connect it even when I created constrictors for the hashMaps. This is very important as the hashMap will be used on several files within the program hence why it is not in the main dart. Therefore I would like your guys input on how I could connect the two files.
This is part of my code:
main dart file:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
//Always use Stateless first then use stateful or stateless widgets afterward
#override
Widget build(BuildContext context) {
return MaterialApp(
//only used at the beginning of the program
title: 'The Cafe',
//just a title to the app it does not show for there is nothing telling it to show on the screen
debugShowCheckedModeBanner: false,
//takes out the ribbon at the top right corner of the screen and app
theme: ThemeData(
primarySwatch: Colors.green,
brightness: Brightness.dark,
fontFamily: 'georgia',
textTheme: TextTheme(headline1: TextStyle(fontSize: 100))
//controls the color of the very top part of the application
),
home: StartPage(),
//used to connect the Stateless widget to the Stateful widget below
);
}
}
class StartPage extends StatefulWidget {
#override
_StartPageState createState() => _StartPageState();
}
// do not forget the } prior to this comment if you do it will result in error and the program does not known why either
class _StartPageState extends State<StartPage> {
String value = "";
//stating the string is not seen until you have started to compute the drop-downs
//have the drop down's take you to the item page
//void main(){
//HashMap map = new HashMap<String, double>();
// LinkedHashMap linkedHashMap = new LinkedHashMap<int, String>();
// SplayTreeMap treeMap = new SplayTreeMap<int, String>();
//}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('The Campus Cafe'),
//where the main title is computed to be shown on the screen
centerTitle: true,
//centers the title
),
body: Center(
//This is Header that is after the main Title
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
//Header Container
Expanded(
child: Image.asset('assets/images/campus-cafe-logo-350sidebar.png',)
),
Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
child: Text("Our Menu",style: TextStyle(fontSize: 30),
),
),
Expanded(
//Expands is used to create a body if you want a header and body...can also be used for other things but at the moment this is all I know
child: Column(
//there can be different types of Columns
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Flexible(
//Padding is how far away one container or item is away from another as shown below
flex:3,
child: DropdownButton<String>(
//items are basically like an array or list
items: [
DropdownMenuItem<String>(
value: "1",
child: Center(
child: Text('Grilled Cheese'),
),
),
DropdownMenuItem<String>(
value: "2",
child: Center(
child: Text('Grilled Ham & Cheese'),
),
),
DropdownMenuItem<String>(
value: "3",
child: Center(
child: Text('BLT'),
),
),
DropdownMenuItem<String>(
value: "4",
child: Center(
child: Text('Western Chicken Sandwich'),
),
),
DropdownMenuItem<String>(
value: "5",
child: Center(
child: Text('Crispy Chicken Wrap'),
),
),
DropdownMenuItem<String>(
value: "6",
child: Center(
child: Text('Cheese Steak'),
),
),
],
onChanged: (_value) => {
print(_value.toString()),
setState(() {
value = _value;
}),
},
hint: Text('Sandwiches')
//This hint displays on your drop-box before you open it to see the items list
),
),
Flexible(
flex:3,
child: DropdownButton<String>(
items: [
DropdownMenuItem<String>(
value: "1",
child: Center(
child: Text('Quantum Burger'),
),
),
DropdownMenuItem<String>(
value: "2",
child: Center(
child: Text('Cheeseburger'),
),
),
DropdownMenuItem<String>(
value: "3",
child: Center(
child: Text('Double Cheeseburger 1/4'),
),
),
DropdownMenuItem<String>(
value: "4",
child: Center(
child: Text('Hamburger 1/4'),
),
),
DropdownMenuItem<String>(
value: "5",
child: Center(
child: Text('Cheeseburger'),
),
),
DropdownMenuItem<String>(
value: "6",
child: Center(
child: Text('Veggie Burger'),
),
),
],
onChanged: (_value) => {
print(_value.toString()),
setState(() {
value = _value;
}),
},
hint: Text('Burgers')),
),
This is my hashMap file:
List<String> sandwich = ["Veggie Melt", "Crispy Chicken Wrap", "Italian Meatball Sub",
"Chicken Parm Grinder", "Grill Cheese", "Grilled Ham & Cheese", "Bacon Bagel Melt"];
List<double> sandwichPrice = [4.50, 6.95, 6.99, 6.59, 3.59, 4.59, 5.29];
Map<String, double> map1 = Map.fromIterables(sandwich, sandwichPrice);
List<String> burgers = ["Veggie Burger", "The Quantum Burger", "Cafe Melt",
"The Bull Rider", "Double Cheese Burger", "Hamburger"];
List<double> burgerPrice = [4.99, 7.25, 6.59, 5.79, 5.89, 3.99, 3.79];
Map<String, double> map2 = Map.fromIterables(burgers, burgerPrice);
List<String> otherItems = ["Chicken Quesadilla", "Cheese Quesadilla",
"Chicken Strips", "Popcorn Chicken", "Jalapeno Poppers"];
List<double> otherItemsPrice = [6.79, 6.29, 4.99, 4.59, 3.49];
Map<String, double> map3 = Map.fromIterables(otherItems, otherItemsPrice);
List<String> sides = ["French Fries", "Onion Rings", "Jalapeno Cheese Curds",
"Tater Tots", "Pretzel Bites", "Nachos & Cheese"];
List<double> sidesPrice = [3.29, 4.79, 4.99, 3.19, 4.59, 3.50];
Map<String, double> map4 = Map.fromIterables(sides, sidesPrice);
List<String> pizza = ["7-inch Cheese", "7-inc with topping"];
List<double> pizzaPrice = [4.59, 4.99];
Map<String, double> map5 = Map.fromIterables(pizza, pizzaPrice);
class Menu {
String sandwich;
String burger;
String otherItems;
String sides;
String pizza;
double sandwichPrice;
double burgerPrice;
double otherItemsPrice;
double sidesPrice;
double pizzaPrice;
Menu.s(this.sandwich, this.sandwichPrice){}
Menu.b(this.burger, this.burgerPrice){}
Menu.o(this.otherItems, this.otherItemsPrice){}
Menu.q(this.sides, this.sidesPrice){}
Menu.p(this.pizza, this.pizzaPrice){}
}
First of all, you need to import your hashMap file. Then update your MyWidget as follows:
I have displayed here an example of how you can use the Dropdown with map1 (i.e, Sandwiches).
You have to just iterate over the keys of the hashmap 'map1' & create the list of DropdownMenuItem from it & pass this list to the items property.
For each dropdown, you will need to save the selected option, so instead of using String value = '';, I have changed it to String selectedSandwich = 'Sandwiches';. This part is crucial as the Dropdown widget can have its value only as one of the options available. So, if you do not have the Sandwiches option in your dropdown list, there will be an error. Hence, I have added the Sandwiches option in the initState of your MyAppWidget.
You have to do the same for the remaining of the hashmaps. Let me know if you need any more help.
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Instead of value use selectedSandwich
String selectedSandwich = 'Sandwiches';
// List of sandwiches, you have to do the same for rest of the maps
// like: burgers, otherItems etc.
List<String> sandwiches = map1.keys.toList();
#override
void initState() {
super.initState();
// Adding sandwiches as an option is necessay as the dropdown's value
// must be equal to one of its options.
// I have done this only for sandwiches, but you need to do the same
// for rest.
sandwiches.insert(0, 'Sandwiches');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DropdownButton<String>(
items: sandwiches // using map1.keys.toList
.map(
(e) => DropdownMenuItem<String>(
value: e,
child: Center(
child: Text(e),
),
),
)
.toList(),
onChanged: (_value) => {
print(_value.toString()),
setState(() {
selectedSandwich = _value; // Set selected sandwich
}),
},
value: selectedSandwich, // Use value
),
],
),
);
}
}
I have a dropdown menu with several options. For sake of simplicity lets say they are: "A", "B" and "C".
This is the snippet of my code:
children: <Widget>[
FormBuilder(
key: _fbKey,
autovalidate: true,
child: Column(
children: <Widget>[
FormBuilderDropdown(
attribute: "value",
decoration: InputDecoration(
labelText: "Choose something?"),
hint: Text('Select Option'),
validators: [FormBuilderValidators.required()],
items: user.option.map((v) {
return DropdownMenuItem(
value: v,
child: ListTile(
leading: Image.asset(
'assets/img/image.png',
width: 50,
height: 50,
),
title: Text("${v.option}"),
));
}).toList(),
),
],
),
// if v.option == "A" is selected here build Widget A()
// if v.option == "B" is selected here build Widget B()
// if v.option == "C" is selected here build Widget C()
),
So, based on Dropdown selection I want to render appropriate Widget.
How can I build widget A if A is selected in the Dropdown menu, B if B is selected or C if C is selected?
This has to change dynamically though, any help is good.
Most simple way I see it is to create a function that renders your widgets, let's call it _renderWidget(), inside of that function you could have something as follows:
_renderWidget() {
if(condition == A) {
return Text('Widget A'); // this could be any Widget
} else if(condition == B) {
return Text('Widget B');
} else {
return Text('Widget C');
}
}
Then inside your DropdownButton onChanged function, you can change the condition based on the dropdown value:
onChanged(String value) {
if(value == 'something') {
setState(() {
condition = A; // A, B or C
});
}
}
You would call your _renderWidget() function inside the widget where you want to show them, for example let's say a Container widget.
Container(
child: _renderWidget()
)
Of course, all of this needs to be done inside a StatefulWidget.
use a stateful widget. you can have a field that says wich widget to show and call set state every time a different option is selected. all three widgets could be added to the main List<Widget> using collection if as follows:
children: <Widget>[
FormBuilder(
key: _fbKey,
autovalidate: true,
child: Column(
children: <Widget>[
FormBuilderDropdown(
attribute: "value",
decoration: InputDecoration(
labelText: "Choose something?"),
hint: Text('Select Option'),
validators: [FormBuilderValidators.required()],
items: user.option.map((v) {
return DropdownMenuItem(
value: v,
child: ListTile(
leading: Image.asset(
'assets/img/image.png',
width: 50,
height: 50,
),
title: Text("${v.option}"),
));
}).toList(),
),
],
),
),
if (v.option == "A") A(),
if (v.option == "B") B(),
if (v.option == "C") C(),
]
you can make a widget that accept the option in its constructor, and call it under the Drop Down builder
Example :
class RenderOption extends StatelessWidget {
final option;
const RenderOption({Key key, this.option}) : super(key: key);
#override
Widget build(BuildContext context) {
switch (option) {
case 1:
return Container();
break;
case 2:
return Container();
break;
case 3:
return Container();
break;
default:
}
}
}
now in you code above
children: <Widget>[
FormBuilder(
key: _fbKey,
autovalidate: true,
child: Column(
children: <Widget>[
FormBuilderDropdown(
attribute: "value",
decoration: InputDecoration(
labelText: "Choose something?"),
hint: Text('Select Option'),
validators: [FormBuilderValidators.required()],
items: user.option.map((v) {
return DropdownMenuItem(
value: v,
child: ListTile(
leading: Image.asset(
'assets/img/image.png',
width: 50,
height: 50,
),
title: Text("${v.option}"),
));
}).toList(),
),
],
),
//_currentOption is declared above in the widget tree
// it indicates the current selected option
RenderOption(option:_currentOption)
),
Note : you have to make the Parent Widget (which has the column or the listView as a child ) Stateful widget to make the code run properly
Edit : I've added the _currentOption variable which indicated the current selected option and paste it to the RenderOption Widget. you should implement the onChanged function in the FormBuilderDropDown to update the selected option , like this
onChanged: (option) =>setState(()=>_currentOption = option)
I need to have a DropdownButton's list of options open/show programmatically when some other widget is tapped. I know that this may not be UI-best-practice and all, but I need this behavior:
As an example, in a structure like the one below, I may need to have taping Text("every") to open the neighboring DropdownButton's dropdown list, behaviors similar to clicking a <select>'s label in HTML.
Row(children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('every'),
),
Expanded(
child: DropdownButton<String>(
value: _data['every'],
onChanged: (String val) => setState(() => _data['every'] = val),
items: _every_options.map<DropdownMenuItem<String>>(
(String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
},
).toList(),
isExpanded: true,
),
),
]);
NOTE: I am in need though of the general solution to this problem, not just how to make that Text behave somewhat "like a HTML label" in the tree below. It may need to be triggered to open by maybe a further away button etc.
The other answer is the best way to do this, but as requested by the OP in comments, here are two very "hacky" ways to achieve this, yet without implementing custom widgets.
1. Access DropdownButton widget tree directly using GlobalKey
If we look at the source code of DropdownButton, we can notice that it uses GestureDetector to handle taps. However, it's not a direct descendant of DropdownButton, and we cannot depend on tree structure of other widgets, so the only reasonably stable way to find the detector is to do the search recursively.
One example is worth a thousand explanations:
class DemoDropdown extends StatefulWidget {
#override
InputDropdownState createState() => DemoDropdownState();
}
class DemoDropdownState<T> extends State<DemoDropdown> {
/// This is the global key, which will be used to traverse [DropdownButton]s widget tree
GlobalKey _dropdownButtonKey;
void openDropdown() {
GestureDetector detector;
void searchForGestureDetector(BuildContext element) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is GestureDetector) {
detector = element.widget;
return false;
} else {
searchForGestureDetector(element);
}
return true;
});
}
searchForGestureDetector(_dropdownButtonKey.currentContext);
assert(detector != null);
detector.onTap();
}
#override
Widget build(BuildContext context) {
final dropdown = DropdownButton<int>(
key: _dropdownButtonKey,
items: [
DropdownMenuItem(value: 1, child: Text('1')),
DropdownMenuItem(value: 2, child: Text('2')),
DropdownMenuItem(value: 3, child: Text('3')),
],
onChanged: (int value) {},
);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Offstage(child: dropdown),
FlatButton(onPressed: openDropdown, child: Text('CLICK ME')),
],
);
}
}
2. Use Actions.invoke
One of the recent features of Flutter is Actions (I'm not sure what it's meant for, I've only noticed it today after flutter upgrade), and DropdownButton uses it for reacting to different... well, actions.
So a little tiny bit less hacky way to trigger the button would be to find the context of Actions widget and invoke the necessary action.
There are two advantages of this approach: firstly, Actions widget is a bit higher in the tree, so traversing that tree wouldn't be as long as with GestureDetector, and secondly, Actions seems to be a more generic mechanism than gesture detection, so it's less likely to disappear from DropdownButton in the future.
// The rest of the code is the same
void openDropdown() {
_dropdownButtonKey.currentContext.visitChildElements((element) {
if (element.widget != null && element.widget is Semantics) {
element.visitChildElements((element) {
if (element.widget != null && element.widget is Actions) {
element.visitChildElements((element) {
Actions.invoke(element, Intent(ActivateAction.key));
return false;
});
}
});
}
});
}
It's one (of many) designed API limitations...
The easiest approach to accomplish what you want, without modifying the SDK, copy dropdown.dart, and create your own version of it, let's say custom_dropdown.dart, and paste the code there ...
in line 546, rename the class to CustomDropdownButton, and in line 660 and 663 rename _DropdownButtonState to CustomDropdownButtonState, ( we need the state class to be exposed outside the file ).
Now you can do whatever you want with it,
although you were interested in the _handleTap(), to open the overlay menu options.
Instead of making _handleTap() public, and refactor the code, add another method like:
(line 726)
void callTap() => _handleTap();
Now, change your code to use your DropdownButton instead of the Flutter's DropdownButton, the key is to "set the key" (Global one) :P
// some stateful widget implementation.
Map<String, String> _data;
List<String> _every_options;
// we need the globalKey to access the State.
final GlobalKey dropdownKey = GlobalKey();
#override
void initState() {
_every_options = List.generate(10, (i) => "item $i");
_data = {'every': _every_options.first};
simulateClick();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Row(children: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('every'),
),
Expanded(
child: CustomDropdownButton<String>(
key: dropdownKey,
value: _data['every'],
onChanged: (String val) => setState(() => _data['every'] = val),
items: _every_options
.map((str) => DropdownMenuItem(
value: str,
child: Text(str),
))
.toList(),
isExpanded: true,
),
),
]),
);
}
void simulateClick() {
Timer(Duration(seconds: 2), () {
// here's the "magic" to retrieve the state... not very elegant, but works.
CustomDropdownButtonState state = dropdownKey.currentState;
state.callTap();
});
}
When a PopupMenuButton is pressed, the currently selected value is highlighted,
but when a DropdownButton is pressed, the currently selected value is not highlighted.
Is there a way to highlight the selected value of a DropdownButton?
For reference here is some sample code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String letter = 'A';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Popup Menu Button')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 16.0),
Text('PopupMenuButton'),
buildPopupMenuButton(),
SizedBox(height: 16.0),
Text('DropdownButton'),
buildDropdownButton(),
],
),
);
}
Widget buildPopupMenuButton() {
return PopupMenuButton<String>(
padding: EdgeInsets.zero,
initialValue: letter,
onSelected: (val) => setState(() => letter = val),
child: ListTile(
title: Text('The letter $letter'),
),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
PopupMenuItem<String>(
value: 'A',
child: Text('The letter A'),
),
PopupMenuItem<String>(
value: 'B',
child: Text('The letter B'),
),
];
},
);
}
Widget buildDropdownButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: DropdownButton<String>(
value: letter,
onChanged: (val) => setState(() => letter = val),
items: [
DropdownMenuItem<String>(
value: 'A',
child: Text('The letter A'),
),
DropdownMenuItem<String>(
value: 'B',
child: Text('The letter B'),
),
],
),
);
}
}
Here's a video that shows the issue:
The DropdownMenuItem doesn't support many custom modifications on the child element, as there's no style, background, anything actually in the DropdownMenuItem attributes to help you with that. Looking at the code, it really wasn't built for that,
Yet, there's something you could add, a simple check on the child attribute of the DropdownMenuItem, and wrap the Text child element in something else or style the Text element itself if it is checked.
One example:
Widget buildDropdownButton() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: DropdownButton<String>(
value: letter,
onChanged: (val) => setState(() => letter = val),
items: [
DropdownMenuItem<String>(
value: 'A',
child: Container(
color: letter == 'A' ? Colors.black12 : null,
child: Text('The letter A'),
),
),
DropdownMenuItem<String>(
value: 'B',
child: Container(
color: letter == 'B' ? Colors.black12 : null,
child: Text('The letter B'),
),
),
],
),
);
}
Note that in a real case scenario, you would have a method with a paremeter to build each dropdown item, so the verification wouldn't have to be hardcoded like letter == 'A'.
This would be the output:
This approach allows you to style a bit, but it has an ugly result in some cases. Although it is customizable, there will always be a white margin around the item, and it also shows the same styles when the dropdown list is closed, so it gets a bit ugly on the main page.
Instead of changing the background, you can also change text colors, underline, icons on the side, something like that make it much better, like:
DropdownMenuItem<String>(
value: 'A',
child: Text('The letter A',
style: TextStyle(
color: letter == 'A' ? Colors.red : Colors.black87,
),
),
)
Well, as far as I know this grey overlay is a so called 'Ripple effect' in the material design library. It seems that Flutter does not adapt the full design in all widgets yet.
However you can try to use the InkWell widget to add this kind of animations/colors to current widgets:
https://flutter.io/docs/cookbook/gestures/ripples
E.g:
PopupMenuItem<String>(
value: 'B',
child: InkWell(child: Text('The letter B'))
),
I am not sure if the width will be correct, but at least it should show the grey overlay when you press on the entry.
You can also check the Flutter source:
https://github.com/flutter/flutter/blob/237fc2fb45639312001e947bf7465ef9f23bb699/packages/flutter/lib/src/material/popup_menu.dart#L933
Here you can see that a Inkwell is standard being used for the PopupMenuButton.
Responding to your original issue which was: "I'm interested in the darker background from the currently selected value when all of the values are shown."
Your PopupMenuButton will look at its initialValue: parameter each time it is opened--the item corresponding to this value will be highlighted. You will need to update the initialValue parameter each time using the onSelected function.
Make sure the parent widget is a StatefulWidget widget and create a reference to whatever your initialValue is. The PopupMenuButton has an onSelected parameter that takes in a function with parameter String.
Whenever you select an option from the PopupMenuButton, call
setState(() {
...
this.initialValue = value;
});
The full class will look something like this.
Class YourClass extends StatefulWidget {
#override
createState() => _YourClassState();
}
class _YourClassState extends State<YourClass> {
...
String initialValue = 'foo';
#override
Widget build(BuildContext context) {
final items = [
PopupMenuItem(
value: 'foo',
child: Text('foo'),
),
PopupMenuItem(
value: 'nice',
child: Text('nice'),
),
}
return Scaffold(
appBar: ...,
drawer: ...,
body: PopupMenuButton(
icon: ...,
itemBuilder: (_) => items,
initialValue: this.initialValue,
onSelected: (value) => bar(value),
),
);
}
void bar(String value) {
setState(() {
...
this.initialValue = value;
});
}
}
You can wrap the widget with Theme to set a highlight color.
return Theme(
data: ThemeData(highlightColor: Colors.grey[300]),
child: DropdownButton()
You may try it:
class CustomDropdownMenuItem<T> extends DropdownMenuItem<T> {
const CustomDropdownMenuItem({
super.key,
super.onTap,
super.value,
super.enabled = true,
super.alignment,
required this.current,
required super.child,
});
final T current;
#override
Widget build(BuildContext context) {
return Container(
color: current == value ? Theme.of(context).highlightColor : null,
child: super.build(context),
);
}
}
However, the element will not be completely covered in color. You can also add a check on the current device to exclude those that work correctly (web and desktop).
Basically, we have to wait for this issue to be solved.
Update:
Alternatively, you can use color selection if you use Text:
final theme = Theme.of(context);
...
return DropdownMenuItem<AppLocale>(
value: value,
onTap: () => {},
child: Text(
value.name,
style: theme.textTheme.titleMedium?.copyWith(
color: value == current ? theme.colorScheme.secondary : null),
),
);