Flutter: set DropdownButtonFormField selection programatically - flutter

Is there a way to set the value of a DropdownButtonFormField programatically?
I tried manipulating the value property, but it does no show any effect.
Example:
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
TestPage({Key key}) : super(key: key);
#override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
int _selectedId;
List<Item> _items = [];
#override
void initState() {
super.initState();
for (int i = 0; i < 5; i++) {
_items.add(Item(i, "choice " + i.toString()));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Column(
children: [
DropdownButtonFormField(
isExpanded: true,
items: _items.map((item) {
return new DropdownMenuItem(
value: item.id,
child: Text(item.text),
);
}).toList(),
onChanged: (value) {
setState(() => _selectedId = value);
},
value: _selectedId,
decoration: InputDecoration(
labelText: "select me",
),
),
RaisedButton(
child: Text('set selected'),
onPressed: () {
setState(() {
_selectedId = 3;
});
},
),
RaisedButton(
child: Text('get selected'),
onPressed: () {
print(_selectedId.toString());
},
)
],
),
);
}
}
class Item {
int id;
String text;
Item(this.id, this.text);
}
When setting _selectedId to 3 via the button, nothing happens, the dropdown does not update. If I set _selectedId to 3 on variable initialization, the third choice is selected when the page loads.
However I need it programatically.

Related

How to show selected checkbox on prev screen?

I need to display checkboxes selected by the user on the previous page using pop()
I have a function that displays the user's message on the previous page and I need to pass the selected checkboxes in the same way. How to pass them as arguments to pop()?
Screen with checkboxes:
const TextScreen({Key? key}) : super(key: key);
#override
State<TextScreen> createState() => _TextScreenState();
}
class _TextScreenState extends State<TextScreen> {
// initial values for checkboxes
bool _privacy = false;
bool _termsOfUse = false;
// text controller for message input
TextEditingController textController = TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
// go to result screen
void getResult(BuildContext context) {
String valueResult = textController.text;
Navigator.pop(context, valueResult);
}
#override
Widget build(BuildContext context) {
//change state for privacy checkbox
_onPrivacyChange(value) {
setState(() {
_privacy = value!;
});
}
//change state for terms of use checkbox
_onTermsOfUSeChange(value) {
setState(() {
_termsOfUse = value!;
});
}
return Scaffold(
appBar: AppBar(
title: const Text('Enter data'),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: textController,
decoration: const InputDecoration(labelText: 'Message')),
const SizedBox(height: 20),
CheckboxListTile(
title: const Text('Privacy'),
controlAffinity: ListTileControlAffinity.leading,
value: _privacy,
onChanged: _onPrivacyChange,
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: const Text('Terms of use'),
controlAffinity: ListTileControlAffinity.leading,
value: _termsOfUse,
onChanged: _onTermsOfUSeChange,
contentPadding: EdgeInsets.zero,
),
ElevatedButton(
onPressed: () {
getResult(context);
},
child: const Text('Display result'))
],
)),
);
}
}
Screen with results display:
class ResultScreen extends StatefulWidget {
const ResultScreen({Key? key}) : super(key: key);
#override
State<ResultScreen> createState() => _ResultScreenState();
}
class _ResultScreenState extends State<ResultScreen> {
String? _valueText = '';
#override
Widget build(BuildContext context) {
// navigation to next screen
void _navToNextScreen(BuildContext context) async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const TextScreen()),
);
// update widget after result comes back
setState(() {
_valueText = result;
});
}
return Scaffold(
appBar: AppBar(
title: const Text('Results'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_navToNextScreen(context);
},
child: const Text('Enter data'),
),
const SizedBox(height: 50),
Text('Message: $_valueText'),
const SizedBox(height: 20),
Text('Checkboxes: '),
],
)),
);
}
}
I think this should be the job of a simple state management strategy; for communication between separate widgets (in this case, two page widgets), that's the cleanest approach. You should create a common service to which both page widgets are subscribed: one to trigger the changes, the other to capture them and display them, using a ChangeNotifier service along with Consumer widgets, as shown below:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => SelectedData(),
child: MyApp()
)
);
}
class SelectedData extends ChangeNotifier {
bool _privacy = false;
bool _termsOfUse = false;
String _valueResult = '';
bool get privacy => _privacy;
bool get termsOfUse => _termsOfUse;
String get valueResult => _valueResult;
set privacy(bool value) {
_privacy = value;
notifyListeners();
}
set termsOfUse(bool value) {
_termsOfUse = value;
notifyListeners();
}
set valueResult(String value) {
_valueResult = value;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: ResultScreen(),
),
),
);
}
}
class TextScreen extends StatefulWidget {
const TextScreen({Key? key}) : super(key: key);
#override
State<TextScreen> createState() => _TextScreenState();
}
class _TextScreenState extends State<TextScreen> {
// text controller for message input
TextEditingController textController = TextEditingController();
#override
void initState() {
super.initState();
}
#override
void dispose() {
textController.dispose();
super.dispose();
}
// go to result screen
void getResult(BuildContext context) {
Navigator.pop(context);
}
#override
Widget build(BuildContext context) {
SelectedData data = Provider.of<SelectedData>(context, listen: false);
textController.text = data.valueResult;
//change state for privacy checkbox
_onPrivacyChange(value) {
data.privacy = value;
}
//change state for terms of use checkbox
_onTermsOfUSeChange(value) {
data.termsOfUse = value;
}
return Scaffold(
appBar: AppBar(
title: const Text('Enter data'),
),
body: Consumer<SelectedData>(
builder: (context, selectedData, child) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: textController,
onChanged: (value) {
data.valueResult = value;
},
decoration: const InputDecoration(labelText: 'Message')),
const SizedBox(height: 20),
CheckboxListTile(
title: const Text('Privacy'),
controlAffinity: ListTileControlAffinity.leading,
value: selectedData.privacy,
onChanged: _onPrivacyChange,
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: const Text('Terms of use'),
controlAffinity: ListTileControlAffinity.leading,
value: selectedData.termsOfUse,
onChanged: _onTermsOfUSeChange,
contentPadding: EdgeInsets.zero,
),
ElevatedButton(
onPressed: () {
getResult(context);
},
child: const Text('Display result'))
],
));
}
),
);
}
}
class ResultScreen extends StatefulWidget {
const ResultScreen({Key? key}) : super(key: key);
#override
State<ResultScreen> createState() => _ResultScreenState();
}
class _ResultScreenState extends State<ResultScreen> {
#override
Widget build(BuildContext context) {
// navigation to next screen
void _navToNextScreen(BuildContext context) async {
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const TextScreen()),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Results'),
),
body: Consumer<SelectedData>(
builder: (context, selectedData, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_navToNextScreen(context);
},
child: const Text('Enter data'),
),
const SizedBox(height: 50),
Text('Message: ${selectedData.valueResult}'),
const SizedBox(height: 20),
const Text('Checkboxes: '),
Text('Privacy: ${selectedData.privacy}'),
Text('Terms of Use: ${selectedData.termsOfUse}')
],
));
}
),
);
}
}
Here's the output when you implement it this way:
So from what i see is you are only passing one value that is message and you what many values to pass at a time so here the map can be used and as pop() function takes dynamic returns you can pass any thing.
From your example i have created a sample example that will be a working proof which will demostrate the using map for passing data to previous screen.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: ResultScreen(),
);
}
}
class TextScreen extends StatefulWidget {
const TextScreen({Key? key}) : super(key: key);
#override
_TextScreenState createState() => _TextScreenState();
}
class _TextScreenState extends State<TextScreen> {
// initial values for checkboxes
bool _privacy = false;
bool _termsOfUse = false;
// text controller for message input
TextEditingController textController = TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
// go to result screen
void getResult(BuildContext context) {
String valueResult = textController.text;
final data = {
"message":valueResult,
"privacy": _privacy,
'terms':_termsOfUse,
};
Navigator.pop(context, data);
}
#override
Widget build(BuildContext context) {
//change state for privacy checkbox
_onPrivacyChange(value) {
setState(() {
_privacy = value!;
});
}
//change state for terms of use checkbox
_onTermsOfUSeChange(value) {
setState(() {
_termsOfUse = value!;
});
}
return Scaffold(
appBar: AppBar(
title: const Text('Enter data'),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: textController,
decoration: const InputDecoration(labelText: 'Message')),
const SizedBox(height: 20),
CheckboxListTile(
title: const Text('Privacy'),
controlAffinity: ListTileControlAffinity.leading,
value: _privacy,
onChanged: _onPrivacyChange,
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: const Text('Terms of use'),
controlAffinity: ListTileControlAffinity.leading,
value: _termsOfUse,
onChanged: _onTermsOfUSeChange,
contentPadding: EdgeInsets.zero,
),
ElevatedButton(
onPressed: () {
getResult(context);
},
child: const Text('Display result'))
],
)),
);
}
}
class ResultScreen extends StatefulWidget {
const ResultScreen({Key? key}) : super(key: key);
#override
State<ResultScreen> createState() => _ResultScreenState();
}
class _ResultScreenState extends State<ResultScreen> {
String? _valueText = '';
bool _privacyValue =false;
bool _termsOfUse = false;
#override
Widget build(BuildContext context) {
// navigation to next screen
void _navToNextScreen(BuildContext context) async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const TextScreen()),
);
if(result !=null)
{
setState(() {
if(result['message']!=null )_valueText = result['message'];
if(result['privacy']!=null) _privacyValue = result['privacy'];
if(result['terms']!=null) _termsOfUse = result['terms'];
});
}
}
return Scaffold(
appBar: AppBar(
title: const Text('Results'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
_navToNextScreen(context);
},
child: const Text('Enter data'),
),
const SizedBox(height: 50),
Text('Message: $_valueText'),
const SizedBox(height: 20),
Text('Privacy Value: $_privacyValue '),
const SizedBox(height: 20),
Text('Terms Value: $_termsOfUse '),
],
)),
);
}
}
You can make changes as per your needs, So let me know if it works.

Open / close filter menu

I have a code that is responsible for building a menu filter. It allows you to filter data by category and then by subcategory.
Initially, subcategories are in a closed state, but when you click on the arrow, they can be opened. Take a look
But my problem is that if I click on the arrow for any category (Country in my case), then all subcategories open at once. Take a look
It's my code
class _FilterDialogUserState extends State<FilterDialogUser> {
Map<String, List<String>?> filters = {};
bool needRefresh = false;
bool isClickedCountry = false;
#override
void initState() {
super.initState();
filters = widget.initialState;
}
List<FilterItem> children = [
FilterItem('Georgia', subitems: [
FilterItem('Tbilisi'),
FilterItem('Batumi'),
]),
FilterItem('Poland', subitems: [
FilterItem('Warsaw'),
FilterItem('Krakow'),
FilterItem('Wroclaw'),
]),
FilterItem('Armenia', subitems: [
FilterItem('Erevan'),
FilterItem('Gyumri'),
]),
];
// Building a dialog box with filters.
#override
Widget build(BuildContext context) {
return SimpleDialog(
title: const Text('Filters',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontFamily: 'SuisseIntl',
)),
contentPadding: const EdgeInsets.all(16),
// Defining parameters for filtering.
children: [
Column(
children: children.map(
(e) {
return Column(
children: [
InkWell(
onTap: () async {
setState(() {
isClickedCountry = !isClickedCountry;
});
},
child: Row(
children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.subitems.forEach((element) =>
element.selected = value as bool);
e.selected = value as bool;
}),
),
Text(e.text),
const Spacer(),
isClickedCountry
? const Icon(Icons.arrow_circle_up)
: const Icon(Icons.arrow_circle_down)
],
),
),
if (e.subitems.isNotEmpty)
!isClickedCountry
? Container()
: Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: Column(
children: e.subitems.map((e) {
return Row(children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.selected = value as bool;
}),
),
Text(e.text),
]);
}).toList(),
),
)
],
);
},
).toList(),
),
]);
}
}
class FilterItem {
final String text;
bool selected;
List<FilterItem> subitems;
FilterItem(
this.text, {
this.selected = false,
this.subitems = const [],
});
}
Tell me, is it possible to change my code so that not all subcategories are opened, but only the one that the user clicks on?
The each main filter item must be controlled one by one.
Define List isClickedCountry variable
Save and load state from List isClickedCountry variable
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
Widget _buildBody() {
return FilterDialogUser();
}
}
class FilterDialogUser extends StatefulWidget {
FilterDialogUser({Key key}) : super(key: key);
#override
State<FilterDialogUser> createState() => _FilterDialogUserState();
}
class _FilterDialogUserState extends State<FilterDialogUser> {
Map<String, List<String>> filters = {};
bool needRefresh = false;
List<bool> isClickedCountry = List.filled(3, false);
#override
void initState() {
super.initState();
// filters = widget.initialState;
}
List<FilterItem> children = [
FilterItem('Georgia', subitems: [
FilterItem('Tbilisi'),
FilterItem('Batumi'),
]),
FilterItem('Poland', subitems: [
FilterItem('Warsaw'),
FilterItem('Krakow'),
FilterItem('Wroclaw'),
]),
FilterItem('Armenia', subitems: [
FilterItem('Erevan'),
FilterItem('Gyumri'),
]),
];
// Building a dialog box with filters.
#override
Widget build(BuildContext context) {
return SimpleDialog(
title: const Text('Filters',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontFamily: 'SuisseIntl',
)),
contentPadding: const EdgeInsets.all(16),
// Defining parameters for filtering.
children: [
Column(
children: children.map(
(e) {
final int index = children.indexOf(e);
return Column(
children: [
InkWell(
onTap: () async {
setState(() {
isClickedCountry[index] = !isClickedCountry[index];
});
},
child: Row(
children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.subitems.forEach((element) =>
element.selected = value as bool);
e.selected = value as bool;
}),
),
Text(e.text),
const Spacer(),
isClickedCountry[index]
? const Icon(Icons.arrow_circle_up)
: const Icon(Icons.arrow_circle_down)
],
),
),
if (e.subitems.isNotEmpty)
!isClickedCountry[index]
? Container()
: Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: Column(
children: e.subitems.map((e) {
return Row(children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.selected = value as bool;
}),
),
Text(e.text),
]);
}).toList(),
),
)
],
);
},
).toList(),
),
]);
}
}
class FilterItem {
final String text;
bool selected;
List<FilterItem> subitems;
FilterItem(
this.text, {
this.selected = false,
this.subitems = const [],
});
}

Unable to reflect updated parent state in showModalBottomSheet

I am relatively new to Flutter and while I really like it I'm struggling to find a way to have state values in the parent be updated in showModalBottomSheet. I think I understand the issue to be that the values aren't reflecting in showModalBottomSheet when they change in the parent because showModalBottomSheet doesn't get rebuilt when the state updates.
I am storing title and content in the parent because I was also hoping to use it for editing as well as creating todos. I figured the showModalBottomSheet could be shared for both. I am attaching a picture on the simulator. What I am expecting is that when title changes (i.e. is no longer an empty string) then the Add To Do button should become enabled but it currently stays disabled unless I close the modal and re-open it.
Any help or insight would be greatly appreciated. Below is the code in my main.dart file which has showModalBottomSheet and has the state values that need to be passed down. NewToDo contains the text fields in the modal that capture the values and updates the state in main accordingly.
** EDIT **
I have seen this link but it doesn't really explain how to pass state from a parent widget down to a showBottomModalSheet widget, it just shows how to manage state within a showBottomModalSheet widget. My goal is to have the state change from within main to be able to be picked within showBottomModalSheet.
main.dart
import 'package:flutter/material.dart';
import './todoitem.dart';
import './todolist.dart';
import 'classes/todo.dart';
import './newtodo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To Do Homie',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: const MyHomePage(title: "It's To Do's My Guy"),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String content = '';
String title = '';
int maxId = 0;
ToDo? _todo;
final titleController = TextEditingController();
final contentController = TextEditingController();
List<ToDo> _todos = [];
void _addTodo(){
final todo = ToDo (
title: title,
id: maxId,
isDone: false,
content: content
);
if (_todo != null){
setState(() {
_todos[_todos.indexOf(_todo!)] = todo;
});
} else {
setState(() {
_todos.add(todo);
});
}
setState(() {
content = '';
maxId = maxId++;
title = '';
_todo = null;
});
contentController.text = '';
titleController.text = '';
}
#override
void initState() {
super.initState();
titleController.addListener(_handleTitleChange);
contentController.addListener(_handleContentChange);
futureAlbum = fetchAlbum();
}
void _handleTitleChange() {
setState(() {
title = titleController.text;
});
}
void _handleContentChange() {
setState(() {
content = contentController.text;
});
}
void _editTodo(ToDo todoitem){
setState(() {
_todo = todoitem;
content = todoitem.content;
title = todoitem.title;
});
contentController.text = todoitem.content;
titleController.text = todoitem.title;
}
void _deleteToDo(ToDo todoitem){
setState(() {
_todos = List.from(_todos)..removeAt(_todos.indexOf(todoitem));
});
}
void _clear(){
contentController.text = '';
titleController.text = '';
setState(() {
content = '';
title = '';
_todo = null;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Center(
child: Container(
alignment: Alignment.topCenter,
child: ToDoList(_todos, _editTodo, _deleteToDo)
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
print(context);
return Container(child:NewToDo(titleController, contentController, _addTodo, _clear, _todo),);
});
},
child: const Icon(Icons.add),
backgroundColor: Colors.deepPurple,
),
);
}
}
NewToDo.dart
import 'package:flutter/material.dart';
import './classes/todo.dart';
class NewToDo extends StatelessWidget {
final Function _addTodo;
final Function _clear;
final ToDo? _todo;
final TextEditingController titleController;
final TextEditingController contentController;
const NewToDo(this.titleController, this.contentController, this._addTodo, this._clear, this._todo, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return
Column(children: [
TextField(
decoration: const InputDecoration(
labelText: 'Title',
),
controller: titleController,
autofocus: true,
),
TextField(
decoration: const InputDecoration(
labelText: 'Details',
),
controller: contentController,
autofocus: true,
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: titleController.text.isNotEmpty ? () => _addTodo() : null,
child: Text(_todo != null ? 'Edit To Do' : 'Add To Do'),
style: ButtonStyle(
backgroundColor: titleController.text.isNotEmpty ? MaterialStateProperty.all<Color>(Colors.deepPurple) : null,
overlayColor: MaterialStateProperty.all<Color>(Colors.purple),
),
),
Visibility (
visible: titleController.text.isNotEmpty || contentController.text.isNotEmpty,
child: ElevatedButton(
onPressed: () => _clear(),
child: const Text('Clear'),
)),
])
],
);
}
}
TextControllers are listenable. You can just wrap your Column in two ValueListenables (one for each controller) and that will tell that widget to update whenever their values are updated.
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: contentController,
builder: (context, _content, child) {
return ValueListenableBuilder(
valueListenable: titleController,
builder: (context, _title, child) {
return Column(
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Title',
),
controller: titleController,
autofocus: true,
),
TextField(
decoration: const InputDecoration(
labelText: 'Details',
),
controller: contentController,
autofocus: true,
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed:
titleController.text.isNotEmpty ? () => _addTodo() : null,
child: Text(_todo != null ? 'Edit To Do' : 'Add To Do'),
style: ButtonStyle(
backgroundColor: titleController.text.isNotEmpty
? MaterialStateProperty.all<Color>(Colors.deepPurple)
: null,
overlayColor: MaterialStateProperty.all<Color>(Colors.purple),
),
),
Visibility(
visible: titleController.text.isNotEmpty ||
contentController.text.isNotEmpty,
child: ElevatedButton(
onPressed: () => _clear(),
child: const Text('Clear'),
),
),
],
)
],
);
},
);
},
);
Another more general alternative I can think of is to use Provider (or, if you're familiar enough, regular InheritedWidgets) and the pattern suggested in its readme:
class Example extends StatefulWidget {
const Example({Key key, this.child}) : super(key: key);
final Widget child;
#override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
int _count;
void increment() {
setState(() {
_count++;
});
}
#override
Widget build(BuildContext context) {
return Provider.value(
value: _count,
child: Provider.value(
value: this,
child: widget.child,
),
);
}
}
where it suggests reading the count like this:
return Text(context.watch<int>().toString());
Except I'm guessing you can just provide the whole state of the widget to descenents by replacing _count with this to refer to the whole stateful widget. Don't know if this is recommended though.
ValueListenables would be my first choice and then maybe hooks to simplify their use though.

Cannot change Dropdown button value Flutter

I want to get the initial value of the Dropdown button from firebase;
but when I try to set the governorateDDValue = selectedUser.governorate; inside the build method
the value of Dropdown get the value from firebase but I cannot change it
DropdownButton.gif
this my code
class UserInfo extends StatefulWidget {
static const routeName = '/UserInfoScreen';
const UserInfo({Key? key}) : super(key: key);
#override
_UserInfoState createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
late User selectedUser;
final date = DateFormat('yyyy-MM-dd').format(DateTime.now()).toString();
var governorateDDValue;
#override
Widget build(BuildContext context) {
final userList= Provider.of<List<User>>(context);
final userID = ModalRoute.of(context)!.settings.arguments as String;
selectedUser =
userList.firstWhere((user) => user.id == userID);
// this line makes dropdown value always equal to value from firestore
governorateDDValue = selectedUser.governorate;
return Scaffold(
appBar: AppBar(
title: Text('report'),
),
body: SingleChildScrollView(
child: Container(
child: Row(
children: <Widget>[
Text('Governorate',),
Container(height: 5),
DropdownButton<String>(
value: governorateDDValue,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
onChanged: (String? newValue) {
setState(() {
governorateDDValue = newValue!;
});
},
items: Constants.governoratesOfEgypt
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
),
),
);
}
}
thanks in advance
Because you use governorateDDValue = selectedUser.governorate; inside build widget so the Dropdown menu will reset its value every time you change it
the build widget will rebuild and the value of the dropdown will stay equal to the value from firebase
you should use governorateDDValue = selectedUser.governorate; outside the build widget
this code should work will
class UserInfo extends StatefulWidget {
static const routeName = '/UserInfoScreen';
const UserInfo({Key? key}) : super(key: key);
#override
_UserInfoState createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
var loading = false;
late User selectedUser;
final date = DateFormat('yyyy-MM-dd').format(DateTime.now()).toString();
var governorateDDValue;
#override
void initState() {
super.initState();
loading = true;
print('Future.delayed outside');
print(loading);
Future.delayed(Duration.zero, () {
governorateDDValue = selectedUser.governorate;
setState(() {
loading = false;
});
});
}
#override
Widget build(BuildContext context) {
final userList = Provider.of<List<User>>(context);
final userID = ModalRoute
.of(context)!
.settings
.arguments as String;
selectedUser =
userList.firstWhere((user) => user.id == userID);
// this line makes dropdown value always equal to value from firestore
governorateDDValue = selectedUser.governorate;
return Scaffold(
appBar: AppBar(
title: Text('report'),
),
body: SingleChildScrollView(
child: Container(
child: Row(
children: <Widget>[
Text('Governorate',),
Container(height: 5),
DropdownButton<String>(
value: governorateDDValue,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
onChanged: (String? newValue) {
setState(() {
governorateDDValue = newValue!;
});
},
items: Constants.governoratesOfEgypt
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
),
),
);
The reason why you cannot change it is because every time setState is called your build method is called. Therefore your value will always be set to governorateDDValue = selectedUser.governorate; So to allow changes you should place this governorateDDValue = selectedUser.governorate; in iniState
Or what you can do is like this so that it will only set it once
class UserInfo extends StatefulWidget {
static const routeName = '/UserInfoScreen';
const UserInfo({Key? key}) : super(key: key);
#override
_UserInfoState createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
late User selectedUser;
final date = DateFormat('yyyy-MM-dd').format(DateTime.now()).toString();
bool initState = true; // ADD HERE
var governorateDDValue;
#override
Widget build(BuildContext context) {
final userList= Provider.of<List<User>>(context);
final userID = ModalRoute.of(context)!.settings.arguments as String;
selectedUser =
userList.firstWhere((user) => user.id == userID);
// this line makes dropdown value always equal to value from firestore
if(initState){ // ADD HERE
governorateDDValue = selectedUser.governorate;
initState = false; // ADD HERE
}
return Scaffold(
appBar: AppBar(
title: Text('report'),
),
body: SingleChildScrollView(
child: Container(
child: Row(
children: <Widget>[
Text('Governorate',),
Container(height: 5),
DropdownButton<String>(
value: governorateDDValue,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
onChanged: (String? newValue) {
setState(() {
governorateDDValue = newValue!;
});
},
items: Constants.governoratesOfEgypt
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
),
),
);
}

Set value of Dropdown Button manually

I have two widgets which are siblings in a container. One widget is a custom DropdownButton, the other one is a custom IconButton:
Parent widget:
static int _currentValue = 0;
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: GCWDropDownButton(
onChanged: (value) {
setState(() {
_currentValue = value;
});
}
),
),
GCWIconButton(
iconData: Icons.add,
onPressed: () {
print(_currentValue);
setState(() {
_currentValue++;
// <------------- how to set value to Dropdown Button
});
},
),
],
);
}
Dropdown widget:
class GCWDropDownButton extends StatefulWidget {
final Function onChanged;
const GCWDropDownButton({Key key, this.onChanged}) : super(key: key);
#override
_GCWDropDownButtonState createState() => _GCWDropDownButtonState();
}
class _GCWDropDownButtonState extends State<GCWDropDownButton> {
int _dropdownValue = 1;
#override
Widget build(BuildContext context) {
return Container(
child: DropdownButton(
value:_dropdownValue,
icon: Icon(Icons.arrow_downward),
onChanged: (newValue) {
setState(() {
_dropdownValue = newValue;
widget.onChanged(newValue);
});
},
items: ...
),
);
}
}
I want to change the DropdownButton's value to be increased after pressing the IconButton. If it were a TextField I'd use a Controller.
But how can I achieve this with the Dropdown?
You're trying to store the same value in 2 different states: in a parent and in a child one. In your case, it's better to do that in parent's state and to pass current value to the child.
int _currentIndex;
#override
Widget build(BuildContext context) {
...
child: Row(
children: <Widget>[
Expanded(
child: GCWDropDownButton(
currentIndex: _currentIndex,
onChanged: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
GCWIconButton(
iconData: Icons.add,
onPressed: () {
setState(() {
if (_currentIndex == null) {
_currentIndex = 0;
} else {
_currentIndex++;
}
});
},
),
],
)
...
class GCWDropDownButton extends StatefulWidget {
final Function onChanged;
final int currentIndex;
const GCWDropDownButton({Key key, this.onChanged, this.currentIndex}) : super(key: key);
#override
_GCWDropDownButtonState createState() => _GCWDropDownButtonState();
}
class _GCWDropDownButtonState extends State<GCWDropDownButton> {
#override
Widget build(BuildContext context) {
final values = ['one', 'two', 'three'];
final currentValue = widget.currentIndex == null
? null
: values[min(values.length - 1, widget.currentIndex)]; // Not going out of range
return Container(
child: DropdownButton(
value: currentValue,
icon: Icon(Icons.arrow_downward),
onChanged: (newValue) {
setState(() {
widget.onChanged(values.indexOf(newValue));
});
},
items: values.map((v) =>
DropdownMenuItem(
child: Text(v.toString()),
value: v,
key: Key(v.toString())
)
).toList()
),
);
}
}
Or it would be even better to place DropdownButton and GCWIconButton in one stateful widget, so both widgets share the same state:
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: GCWDropDownButton()
),
);
}
}
class GCWDropDownButton extends StatefulWidget {
#override
_GCWDropDownButtonState createState() => _GCWDropDownButtonState();
}
class _GCWDropDownButtonState extends State<GCWDropDownButton> {
int _currentIndex;
final values = ['one', 'two', 'three'];
#override
Widget build(BuildContext context) {
final currentValue = _currentIndex == null ? null : values[_currentIndex];
return Row(
children: <Widget>[
Expanded(
child:Container(
child: DropdownButton(
value: currentValue,
icon: Icon(Icons.arrow_downward),
onChanged: (newValue) {
setState(() {
_currentIndex = values.indexOf(newValue);
});
},
items: values.map((v) =>
DropdownMenuItem(
child: Text(v.toString()),
value: v,
key: Key(v.toString())
)
).toList()
),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
if (_currentIndex == null) {
_currentIndex = 0;
} else
// Not going out of range
if (_currentIndex != values.length - 1) {
_currentIndex++;
}
});
},
),
],
);
}
}