flutter - creating dropdown button with data from props - flutter

i'm learning flutter for some time and i have a problem that i dont know how to solve.
I'm trying to create a dropdown button with data that I pass from the parent,data is a list of words like ['work','hobby','social'] etc.
My problem is the dropdown button after changing the value is still showing the initial one i think the problem is in the place when i initialize the "dropdownValue" because i do it in a build method but i cant acces properties from outside of that widget.
class TaskSheet extends StatefulWidget {
#override
_TaskSheetState createState() => _TaskSheetState();
final List categories;
TaskSheet(this.categories);
**// HERE I RECIVE THE LIST**
}
class _TaskSheetState extends State<TaskSheet> {
String dropdownValue;
// I CANT ASSIGN VALUE HERE BECAUSE USING widget.categories DONT WORK HERE AND IT MUST BE INSIDE BUILD METHOD
#override
Widget build(BuildContext context) {
var categoriesList = widget.categories
.map(
(category) => DropdownMenuItem(
value: category.title,
child: Text(category.title),
),
)
.toList();
dropdownValue = categoriesList[0].value; // THIS VALUE ALWAYS STAY THE SAME
return BottomSheet(
builder: (context) => Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 16),
child: Column(
children: [
Text('Add new task'),
TextField(
decoration: InputDecoration(labelText: 'What you wanna do?'),
),
DropdownButton(
icon: Icon(Icons.keyboard_arrow_down),
focusColor: Theme.of(context).primaryColor,
value: dropdownValue,
onChanged: (newValue) {
setState(() {
dropdownValue = newValue; // THIS HAVE NO IMPACT ON INITAL VALUE
});
},
items: categoriesList,
),
],
),
),
onClosing: () {},
);
}
}
I think if i find a way to acces widget.props from oustide of build method it going to work but I dont know how to do this

You can copy paste run full code below
You can see working demo below
Step 1: Category extends Equatable
import 'package:equatable/equatable.dart';
class Category extends Equatable {
String title;
Category({this.title});
#override
List<Object> get props => [title];
}
Step 2: dropdownValue is Category not String and use initState()
Category dropdownValue;
#override
void initState() {
dropdownValue = widget.categories[0];
super.initState();
}
Step 3: value is category
DropdownMenuItem<Category>(
value: category,
working demo
full code
import 'package:flutter/material.dart';
import 'package:equatable/equatable.dart';
class Category extends Equatable {
String title;
Category({this.title});
#override
List<Object> get props => [title];
}
class TaskSheet extends StatefulWidget {
#override
_TaskSheetState createState() => _TaskSheetState();
final List<Category> categories;
TaskSheet(this.categories);
}
class _TaskSheetState extends State<TaskSheet> {
Category dropdownValue;
#override
void initState() {
dropdownValue = widget.categories[0];
super.initState();
}
#override
Widget build(BuildContext context) {
var categoriesList = widget.categories
.map(
(category) => DropdownMenuItem<Category>(
value: category,
child: Text(category.title),
),
)
.toList();
return BottomSheet(
builder: (context) => Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 16),
child: Column(
children: [
Text('Add new task'),
TextField(
decoration: InputDecoration(labelText: 'What you wanna do?'),
),
DropdownButton<Category>(
icon: Icon(Icons.keyboard_arrow_down),
focusColor: Theme.of(context).primaryColor,
value: dropdownValue,
onChanged: (newValue) {
setState(() {
dropdownValue =
newValue; // THIS HAVE NO IMPACT ON INITAL VALUE
});
print(dropdownValue.title);
},
items: categoriesList,
),
],
),
),
onClosing: () {},
);
}
}
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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TaskSheet([
Category(title: "work"),
Category(title: "hobby"),
Category(title: "social")
]),
],
),
),
);
}
}

Related

How to update screen when instance of external stateful widget class is updated

I am displaying the weight of an instance of a person class on my homepage. When I update the weight of this instance through a form in a popup bottom sheet the displayed weight is only changed after a hot reload. How can I trigger a setState in my person class when its instances parameters are changed in homepage?
main.dart
import 'package:flutter/material.dart';
import 'package:metricwidget/screens/homepage.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// Root of application
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Homepage(),
);
}
}
person.dart
import 'package:flutter/material.dart';
class person extends StatefulWidget {
int? weight;
person({Key? key, this.weight}) : super(key: key);
void updateWeight(newWeight){
weight = newWeight;
}
#override
_personState createState() => _personState();
}
class _personState extends State<person> {
#override
Widget build(BuildContext context) {
return Center(
child: Text(
widget.weight.toString(),
style: const TextStyle(fontSize: 24),
),
);
}
}
homepage.dart
import 'package:mvs/person.dart';
import 'package:flutter/material.dart';
class Homepage extends StatefulWidget {
const Homepage({Key? key}) : super(key: key);
#override
_HomepageState createState() => _HomepageState();
}
class _HomepageState extends State<Homepage> {
var joe = person(weight: 23);
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Material(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
child: joe,
),
OutlinedButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) {
return Form(
key: _formKey,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextFormField(
onSaved: (String? value) {
if (int.parse(value!) > 0) {
setState(() {
joe.updateWeight(int.parse(value));
});
}
},
keyboardType: TextInputType.number,
maxLength: 3,
initialValue: joe.weight.toString(),
decoration: const InputDecoration(
icon: Icon(Icons.label),
),
validator: (value) {
if (value!.isEmpty) {
return "Please enter value";
}
return null;
},
),
),
OutlinedButton(
onPressed: () {
_formKey.currentState!.save();
Navigator.pop(context);
},
child: const Text("submit"),
)
],
),
);
},
);
},
child: const Text("Update"),
)
],
),
);
}
}
Was able to solve this using provider and changenotifier, same as the format outlined in the docs below
Reference: https://pub.dev/packages/provider

DropdownButton doesn't re-render the menu when items change

DropdownButton doesn't reflect menuItem's changes when the dropdown menu is open.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final disabledItems = ['Free', 'Four'];
List<String> items = ['One', 'Two', 'Free', 'Four'];
String dropdownValue = 'One';
#override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: dropdownValue,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String newValue) {
if (!disabledItems.contains(newValue)) {
setState(() {
dropdownValue = newValue;
});
}
},
items: items.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Row(children: [
Text(
value,
style: TextStyle(
color: disabledItems.contains(value) ? Colors.grey : null,
),
),
IconButton(
icon: Icon(Icons.delete),
color: Colors.black38,
onPressed: () {
setState(() {
items.removeWhere((element) => element == 'Two');
});
print(items.length);
},
)
]),
);
}).toList(),
);
}
}
What I aim is the chance of removing an item from the menu when the delete icon is pressed. All the expected events are working as expected and the DropDown items list is updating accordingly in the backend but it doesn't re-render.
DorpDown Menu with delete icon
In order to be able to see the updated items list I have to close the dropdown menu and open it again but this doesn’t feel right in terms of user experience.

FormBuilderCheckboxList internal value is reset but the checkbox still in checked condition (not cleared) if using INITIAL VALUE

I am using FLUTTER_FORM_BUILDER package for my custom form. I build checkbox list using FormBuilderCheckbox, i give it initial value using initialValue construtor. The problem occur when I'm trying to clear the checkbox. I use globalkey.currentState.reset() to reset the value. It does reset the internal value of the checkbox, but it seems the checkboxes still in Checked Condition.
How can i clear it? I can't use .clear() since i can't assign controller to the FormBuilderCheckbox.
Any insight would be appreciated, thank you.
EDIT: This is a simplified code to reproduce.
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Map _initialData = {
'checkbox': ['1'],
};
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyCustomForm(data: _initialData),
);
}
}
class MyCustomForm extends StatefulWidget {
final Map data;
const MyCustomForm({Key key, #required this.data}) : super(key: key);
#override
_MyCustomFormState createState() => _MyCustomFormState();
}
class _MyCustomFormState extends State<MyCustomForm> {
List _checkboxInitial;
final GlobalKey<FormBuilderState> _fbKey = GlobalKey<FormBuilderState>();
#override
void initState() {
setState(() {
_checkboxInitial = List.from(widget.data['checkbox']);
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Container(
height: 200,
width: 200,
child: FormBuilder(
key: _fbKey,
child: Column(
children: <Widget>[
FormBuilderCheckboxList(
initialValue: _checkboxInitial,
decoration: InputDecoration(border: InputBorder.none),
attribute: 'checkbox',
options: [
'1',
'2',
'3',
]
.map(
(data) => FormBuilderFieldOption(
child: Text(data),
value: data,
),
)
.toList(growable: false),
),
RaisedButton(
onPressed: () {
setState(() {
_fbKey.currentState.reset();
});
},
child: Text('Clear'),
),
],
),
),
),
);
}
}
Apparently, i solved my own problem using UniqueKey(). To reset the value, just reset it inside setState().
class _MyCustomFormState extends State<MyCustomForm> {
List _checkboxInitial;
final GlobalKey<FormBuilderState> _fbKey = GlobalKey<FormBuilderState>();
#override
void initState() {
setState(() {
_checkboxInitial = widget.data['checkbox'];
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Container(
height: 200,
width: 200,
child: FormBuilder(
key: _fbKey,
child: Column(
children: <Widget>[
FormBuilderCheckboxList(
key: UniqueKey(),
initialValue: _checkboxInitial,
decoration: InputDecoration(border: InputBorder.none),
attribute: 'checkbox',
options: [
'1',
'2',
'3',
]
.map(
(data) => FormBuilderFieldOption(
child: Text(data),
value: data,
),
)
.toList(growable: false),
),
RaisedButton(
onPressed: () {
setState(() {
_checkboxInitial = [];
});
},
child: Text('Clear'),
),
],
),
),
),
);
}
}
I hope it can help anyone who needs it.

Constructor of screen is called every time a change occurs in that screen

When tapping a textformfield in a pushed screen, the constructor of the screen is called again and the textformfield loses its value. Also, I think that every change happens in that screen causes its constructor to be called again, and I don't know the reason at all.
Here is a sample code that generates the error:
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
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
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Hello',
style: TextStyle(color: Colors.black, fontSize: 30.0),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => NextScreen(Bloc())));
},
child: Icon(Icons.add),
),
);
}
}
And here is the screen to be pushed
class NextScreen extends StatefulWidget {
final _bloc;
NextScreen(this._bloc);
#override
_NextScreenState createState() => _NextScreenState();
}
class _NextScreenState extends State<NextScreen> {
#override
void dispose() {
widget._bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(100.0),
child: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: Icon(Icons.arrow_back),
iconSize: 20.0,
),
),
StreamBuilder<String>(
stream: widget._bloc.stream,
builder: (context, snapshot) {
return TextFormField(
controller: widget._bloc.controller,
onFieldSubmitted: widget._bloc.submitData(),
decoration: InputDecoration(
hintText: 'Enter your name..',
errorText: snapshot.data,
),
);
})
],
),
);
}
}
A bloc that validates the user input
class Bloc {
TextEditingController _controller;
TextEditingController get controller => _controller;
BehaviorSubject<String> _subject;
BehaviorSubject<String> _validatorSubject;
Stream<String> get stream => _validatorSubject.stream;
void submitData() {
_subject.sink.add(controller.text);
}
void _validate(String text) {
if (!RegExp(r'[0-9]').hasMatch(text)) {
_validatorSubject.sink.add('numbers only');
} else {
_validatorSubject.sink.add(null);
}
}
Bloc() {
_controller = TextEditingController();
_subject = BehaviorSubject<String>();
_validatorSubject = BehaviorSubject<String>();
_subject.stream.listen(_validate);
}
void dispose() {
_subject.close();
_validatorSubject.close();
}
}
Opening and closing a keyboard will rebuild the whole screen.
The real culprit here is the textController :
controller: widget._bloc.controller,
The solution which worked for me is to remove this line.
Also to get and validate the changed text you can use onChanged in the text field, which return a String.
Like this :
.
.
.
return TextFormField(
onChanged: widget._bloc.submitData,
decoration: InputDecoration
.
.
.
And you submitData() method would go like this :
void submitData(String data) {
_subject.sink.add(data);
}

Flutter Trouble with multiselect checkboxes - data from Firestore

The following code displays the items listed in my collection (Firestore)
I am attempting to create the ability to check any item(s) and then have those items store into a "Favorites" on the next screen.
Currently, the checkboxes are an all or nothing. Either all items are unchecked or checked once tapped.
class _SelectScreenState extends State<SelectScreen> {
bool _isChecked = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Select Exercises')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('exercises').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot)
{
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context,
data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
return Padding(
key: ValueKey(record.name),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
child: ListTile(
title: Text(record.name),
trailing: Checkbox(
value: _isChecked,
onChanged: (bool value) {
setState(() {
_isChecked = value;
});
},
)
),
),
);
}
}
class Record {
final String name;
final DocumentReference reference;
Record(this.name, this.reference);
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['name'] != null),
name = map['name'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
#override
String toString() => "Record<$name:>";
}
It is because you are making use of a single variable for all the checkboxes.
To fix that you could create a dedicated stateful widget, which would handle the state of each of the checkbox's separately from the rest.
So you could replace your ListTile with something like
Exercise(
title: record.name,
)
and then you could define the Exercise widget as follows
class Exercise extends StatefulWidget {
final String title;
Exercise({this.title});
#override
_ExerciseState createState() => _ExerciseState();
}
class _ExerciseState extends State<Exercise> {
bool selected = false;
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.title),
trailing: Checkbox(
value: selected,
onChanged: (bool val) {
setState(() {
selected = val;
});
}),
);
}
}
Here is a complete working example
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
children: <Widget>[
Exercise(
title: "Exercises 1",
),
Exercise(
title: "Exercises 2",
),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class Exercise extends StatefulWidget {
final String title;
Exercise({this.title});
#override
_ExerciseState createState() => _ExerciseState();
}
class _ExerciseState extends State<Exercise> {
bool selected = false;
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.title),
trailing: Checkbox(
value: selected,
onChanged: (bool val) {
setState(() {
selected = val;
});
}),
);
}
}
Because you have a global variable _isChecked, this needs to be created with each listTile.
Try moving the variable
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
bool _isChecked = false; //try moving it here
return Padding(
key: ValueKey(record.name),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
child: ListTile(
title: Text(record.name),
trailing: Checkbox(
value: _isChecked,
onChanged: (bool value) {
setState(() {
_isChecked = value;
});
},
)
),
),
);
}