Hi I am new to flutter and have been going through flutter's udacity course building their unit converter app to try to learn about the framework. I was attempting to architecture the app using bloc but have ran into an issue with my dropdown menu. Every time when I change the item in the dropdown it resets back to the default value when focusing on the text input field. It looks like the widget tree i rebuilt when focusing on a textfield. The default units are the reset because in my bloc constructor I have a method to set default units. I am at a loss for where I would move my default units method so that it does not conflict. What should I do in my bloc to set default units only when a distinct category is set, and when it is first being built.
I tried using _currentCatController.stream.distinct method to only update the stream when distinct data is passed but that did not seem to work either. I tried to wrap the default units method in various conditional statements that did not give me the result I wanted.
you can find all the source here https://github.com/Renzo-Olivares/Units_Flutter
class _ConverterScreenState extends State<ConverterScreen> {
///function that creates dropdown widget
Widget _buildDropdown(
bool selectionType, ValueChanged<dynamic> changeFunction) {
print("build dropdown");
return Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, style: BorderStyle.solid),
borderRadius: BorderRadius.circular(4.0)),
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: StreamBuilder<Unit>(
stream: _conversionBloc.inputUnit,
initialData: widget._category.units[0],
builder: (context, snapshotIn) {
return StreamBuilder<Unit>(
stream: _conversionBloc.outputUnit,
initialData: widget._category.units[1],
builder: (context, snapshotOut) {
return StreamBuilder<Category>(
stream: _conversionBloc.currentCategory,
initialData: widget._category,
builder: (context, snapshotDropdown) {
return DropdownButton(
items: snapshotDropdown.data.units
.map(_buildDropdownItem)
.toList(),
value: selectionType
? snapshotIn.data.name
: snapshotOut.data.name,
onChanged: changeFunction,
isExpanded: true,
hint: Text("Select Units",
style: TextStyle(
color: Colors.black,
)),
);
});
});
})),
),
),
);
}
}
class ConversionBloc {
//input
final _currentCatController = StreamController<Category>();
Sink<Category> get currentCat => _currentCatController.sink;
final _currentCatSubject = BehaviorSubject<Category>();
Stream<Category> get currentCategory => _currentCatSubject.stream;
ConversionBloc() {
print("conversion bloc");
//category
_currentCatController.stream.listen((category) {
print("setting category ${category.name}");
_category = category;
_currentCatSubject.sink.add(_category);
//units default
setDefaultUnits(_category);
});
}
void setDefaultUnits(Category category) {
print("setting default units for ${category.name}");
_inputUnits = category.units[0];
_outputUnits = category.units[1];
_inputUnitSubject.sink.add(_inputUnits);
_outputUnitSubject.add(_outputUnits);
}
}
The issue here is that the DropdownButton value wasn't updated on onChanged. What you can do here is handle the value passed from onChanged and update the DropdownButton value. Also, focusing on a Widget displayed on screen shouldn't rebuild Widget build().
Related
I want to be able to edit the TextField and after all of them are filled, a value is calculated. Every time the user change a value, the value must be recalculated.
I've tried with both the Streams and Controller. The stream was doing fine, even tho the update of the value was done with data's that were 1 change old.
i.e. height, weight and age all must be != null, when we have at first {height: 2, weight:3, age:2}, it doesn't get calculated, after one of them is changed then the result is calculated but with the above data.
The controller instead doesn't seem to listen on the variable change at all.
Here I initialize the controller in another class
class Bsmi extends StatelessWidget {
StreamController<void> valueBoxStream = StreamController<void>.broadcast();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new DefaultAppBar(context: context, title: Text(" ")),
body: new ListView(
children: <Widget>[
BsmiResult(valueBoxStream, {}),
new Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
),
child: ListView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
padding: EdgeInsets.all(20.0),
itemExtent: 80.0,
children: <Widget>[
_BsmiResultState().indices("height", valueBoxStream),
_BsmiResultState().indices("weight", valueBoxStream),
_BsmiResultState().indices("age", valueBoxStream),
],
),
),
],
),
);
}
}
This is the important code
class BsmiResult extends StatefulWidget {
final StreamController<void> valueBoxStream;
const BsmiResult(this.valueBoxStream, this.data);
final Map data;
#override
_BsmiResultState createState() =>
_BsmiResultState(valueBoxStream: this.valueBoxStream, data: this.data);
}
class _BsmiResultState extends State<BsmiResult> {
_BsmiResultState({this.valueBoxStream, this.data});
StreamController<void> valueBoxStream;
final Map data;
final textFieldController = new TextEditingController();
int weight;
int age;
int height;
String _result = "0.0";
_printLatestValue() {
print("Second text field: ${textFieldController.text}");
}
void setData(data) {
if (data['type'] == 'height') {
print("I'm inside height\n");
height = int.parse(data['value']);
} else if (data['type'] == 'weight') {
print("I'm inside weight\n");
weight = int.parse(data['value']);
} else {
print("I'm inside age\n");
age = int.parse(data['value']);
}
}
#override
void initState() {
super.initState();
textFieldController.addListener(_printLatestValue);
valueBoxStream.stream.listen((_){
_calculateResult();
});
}
#override
void dispose() {
// Clean up the controller when the Widget is removed from the Widget tree
textFieldController.dispose();
super.dispose();
}
void _calculateResult() {
if ((height != null) && (height != 0) && (weight != null) && (weight != 0) && (age != null) && (age != 0)) {
print("height: " +
height.toString() +
"" +
" weight: " +
weight.toString() +
" age: " +
age.toString());
_result = ((height + weight) / age).toString();
print(_result + "\n");
} else {
_result = "0.0";
}
}
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Text("Risultato"),
new Container(
decoration: new BoxDecoration(
borderRadius: BorderRadius.circular(15.0),
), //For the controller i don't use the StreamBuilder but a normal Text("$_result"),
child: StreamBuilder(
stream: valueBoxStream.stream,
builder: (context, snapshot) {
if (snapshot.hasData || _result != "0.0") {
print(snapshot.data);
setData(snapshot.data);
return new Text(
"$_result",
textAlign: TextAlign.end,
);
} else {
return new Text(
"0.0",
textAlign: TextAlign.end,
);
}
},
),
),
],
);
}
Row indices(String text, StreamController<void> valueBoxStream) {
return new Row(
children: <Widget>[
leftSection(text),
rightSection(text, valueBoxStream),
],
);
}
Widget leftSection(text) {
return new Expanded(
child: new Container(
child: new Text(
text,
style: TextStyle(fontSize: 18),
),
),
);
}
Container rightSection(text, valueBoxStream) {
return new Container(
child: new Flexible(
flex: 1,
child: new TextField(
controller: textFieldController,
keyboardType: TextInputType.number,
textAlign: TextAlign.end,
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter(new RegExp("[0-9.]")),
LengthLimitingTextInputFormatter(4),
],
decoration: new InputDecoration(
enabledBorder: new OutlineInputBorder(
borderRadius: new BorderRadius.circular(15),
borderSide: new BorderSide(width: 1.2),
),
border: new OutlineInputBorder(
borderRadius: new BorderRadius.circular(15),
borderSide: new BorderSide(color: Colors.lightBlueAccent),
),
hintText: '0.0',
),
onChanged: (val) {
valueBoxStream.add({'type': text, 'value': val});
},
),
),
);
}
}
I should be able at this point to get Result: x.y, but I get it only once after I change the textField only once and in the same 'session'
Thanks in advance who can really explain to me why this is not working and what errors I'm making.
I'm really sorry.. but there have been so many things wrong with that code, I'm not even sure where to begin. It looks like you read all versions on how to handle state in flutter and managed to combine it all in one simple use case.. Here is one working example fix: https://gitlab.com/hpoul/clinimetric/commit/413d32d4041f2e6adb7e30af1126af4b05e72271
I'm not sure how to answer this "question" so that others could possibly benefit from it. but..
If you create a stream controller or a text view controller.. You have state.. hence, create a stateful widget (otherwise you can't for example close streams and subscriptions)
Get a handle on which widget is responsible for which state. And pass down only useful values to child views. One possible architecture is to use a ViewController and use Sinks as inputs for change events, and Streams for handling those events. So if you create a stream controller, only use a streamController.sink if you want to notify of events, and use a streamController.stream if a widget needs to listen to events
the build method is not the right place to handle events. I refactored your code that the handling of events and calculation of events is done in the listener of the stream. If you want to use a StreamBuilder you could have a StreamController and your producers directly push the Result into the sink.. so the builder method of the StreamBuilder would only need to do Text('${snapshot.data.value}')
There are probably quite a few things i'm missing in that list. but that should get you started. look at my change set. It is not perfect, but it works. You should try to understand what you are doing and reduce complexity instead of stacking everything you find on top of each other.
On my grid items when I click, it opens a ModalBottomSheet and listed with filter chips of strings. When you click a filter chip value, the value is updated but the widget does not re-render. The app is a StatefulWidget.
I have called the function setState.
What I expect is filterchips becomes checked and unchecked on selection.
void _showBottom(index){
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context){
return new Container(
padding: new EdgeInsets.all(27.0),
child: new Column(
children: <Widget>[
new Text('Some headline', style: new TextStyle( fontWeight: FontWeight.bold, fontSize: 22),),
getFilterChipsWidgets(index),
],
),
);
}
);
}
Widget getFilterChipsWidgets(index)
{
List<Widget> tags_list = new List<Widget>();
for(var i=0; i<list[index]["tags"].length; i++) {
var _isSelected = true;
FilterChip item = new FilterChip(
label: Text("Filtertext",),
selected: _isSelected,
onSelected: (bool newValue) {
setState(() {
_isSelected = !_isSelected;
debugPrint(_isSelected.toString());
});
},
);
tags_list.add(item);
}
return new Row(children: tags_list);
}
You need to add height for the root node of the bottom sheet. So change your container to have a fixed height.
Container(
height: 300.0, // Add height
padding: new EdgeInsets.all(27.0),
child: new Column(
children: <Widget>[
new Text('Some headline',
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 22),),
getFilterChipsWidgets(index),
],
),
I usually calculate this dynamically based on the widgets I'm passing in. But this should get you started.
Edit
The comment I gave below was the actual answer.
You should wrap all the widgets in the bottom sheet into it's own stateful widget and set your values in there.
Modal Bottom sheet seems to call the constructor whenever any change in the UI occurs in the child widget for example changing focus inside a form.
So you need to create instances of the objects you are working with inside a parent StatefulWidget and then to reflect changes in the UI of the Model Bottom Sheet you need to call setState whenever you make changes to the data.
You need to wrap the under StatefulBuilder like this
import 'package:flutter/material.dart';
void _showBottom(index){
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context){
return StatefulBuilder( builder: (BuildContext context, StateSetter setState){
return new Container(
padding: new EdgeInsets.all(27.0),
child: new Column(
children: <Widget>[
new Text('Some headline', style: new TextStyle( fontWeight: FontWeight.bold, fontSize: 22),),
getFilterChipsWidgets(index, setState),
],
),
);
});
}
);}
Widget getFilterChipsWidgets(index, StateSetter setState)
{
List<Widget> tags_list = new List<Widget>();
for(var i=0; i<list[index]["tags"].length; i++) {
var _isSelected = true;
FilterChip item = new FilterChip(
label: Text("Filtertext",),
selected: _isSelected,
onSelected: (bool newValue) {
setState(() {
_isSelected = !_isSelected;
debugPrint(_isSelected.toString());
});
},
);
tags_list.add(item);
}
return new Row(children: tags_list);
}
I have a Widget that renders a ListView of TextFormFields to input data. However, I want to change the keyboard type for one of my textfields dynamically. Here is an overview of the Widget:
class CreateTodo extends StatefulWidget {
#override
_CreateTodoState createState() => _CreateTodoState();
}
class _CreateTodoState extends State<CreateTodo> {
final _formKey = GlobalKey<FormState>();
final double formWidth = 175.0;
final Map<String, Icon> itemsMap = {
'Heading': Icon(Icons.title),
'Description': Icon(Icons.edit),
'Date': Icon(Icons.calendar_today)
};
final items = ['Heading', 'Description', 'Date'];
_changeKeyboardType(entry) {
if (entry == "Description") {
return TextInputType.multiline;
}
return TextInputType.text;
}
ListView createListItems() {
return ListView.builder(
padding: EdgeInsets.only(
left: 50.0,
right: 50.0,
top: 20.0
),
scrollDirection: Axis.vertical,
itemCount: 3,
itemBuilder: (context, index) {
var currentHeading = this.items[index];
var currentIcon = this.itemsMap[currentHeading];
return Padding(
padding: EdgeInsets.only(bottom: 50.0),
child: new TextFormField(
validator: (input) {
if (input.isEmpty) {
return 'You must enter a ${currentHeading.toLowerCase()} for your to-do';
}
},
keyboardType: this._changeKeyboardType(currentHeading),
decoration: new InputDecoration(
prefixIcon: currentIcon,
labelText: currentHeading,
border: OutlineInputBorder(),
),
)
);
}
);
}
#override
Widget build(BuildContext context) {
return Form(
autovalidate: true,
child: Expanded(
child: (this.createListItems())
)
);
}
}
The main line that isn't working is: keyboardType: this._changeKeyboardType(currentHeading) ... Am I approaching this problem the wrong way? Any direction will be super helpful. I basically want the textFormFeld for the description being multiline so users can have a better visualization of their description.
My working solution for now:
// Where entry is an object containing its focusNode and textInputType
entry.focusNode.unfocus();
setState(() {
entry.textInputType = type;
});
// The delay was necessary, otherwise only the 2nd tap would actually change
Future.delayed(const Duration(milliseconds: 1), () {
FocusScope.of(context).requestFocus(entry.focusNode);
});
Only downside I couldn't fix so far is that the keyboard is disappearing before it re-appears with the new TextInputType.
Google's Spreadsheet app does it instantaneous without the close, would love to know if and how that'll be doable in Flutter.
You'd have to change your logic as my example is asynchronous.
Let me know if that helps or if you need more help.
Or, if you've already figured it out, how you did it
May like this:
Credits: It's inspired by https://stackoverflow.com/a/58498474/3484824
The full page code is very long but my DropdownButton widget code like this.
The problems are,
first: I can't update my selectedCity, it doesn't get an update. Also, the print function calls null, since my cityList data is like [new york, paris, london] etc...
second: flutter doesn't change focus from any TextField to DropdownButton fully. I mean, clicked TextField, then DropdownButton but focus reverts to that TextField after the button click. It is default action of Flutter?
List<dynamic> _cityList;
String _selectedCity;
#override
Widget build(BuildContext context) {
return DropdownButton(
value: _selectedCity,
style: TextStyle(
fontSize: 11,
color: textColor,
),
items: _cityList.map((city) {
return DropdownMenuItem<String>(
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(city),
),
);
}).toList(),
onChanged: (String value) {
setState(() {
_selectedCity = value;
print(_selectedCity);
});
},
isExpanded: true,
);
}
Edit: The solution of resetting FocusNode after selecting an item from DropdownMenuItem is adding this line inside of setstate like this:
this: FocusScope.of(context).requestFocus(new FocusNode());
to here: onChanged:(){setSate((){here}}
I hope it will help you. I have modified your code a little bit
List<dynamic> _cityList;
String _selectedCity;
It will show the Dropdown Button and when you click on it and select any value showing in the print
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
body: ListView(
children: [
Column(
children: <Widget>[
DropdownButton<String>(
items: _cityList.map((dynamic value) {
return DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedCity = value;
print(_selectedCity);
});
},
),
],
),
],
),
);
}
for the focus problem you should use focusNodes one with the drop down list and another with the text field https://docs.flutter.io/flutter/widgets/FocusNode-class.html.
I doing a AlertDialog, so when I tried to insert Slider widget inside the state of value sound realy stranger, and this doesn't happens if Slider is outside of AlertDialog
new Slider(
onChanged: (double value) {
setState(() {
sliderValue = value;
});
},
label: 'Oi',
divisions: 10,
min: 0.0,
max: 10.0,
value: sliderValue,
)
The complete widget code of AlertDialog
Future<Null> _showDialog() async {
await showDialog<Null>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
title: const Text('Criar novo cartão'),
actions: <Widget>[
new FlatButton(onPressed: () {
Navigator.of(context).pop(null);
}, child: new Text('Hello'))
],
content: new Container(
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Text('Deseja iniciar um novo cartão com quantos pedidos ja marcados?'),
new Slider(
onChanged: (double value) {
setState(() {
sliderValue = value;
});
},
label: 'Oi',
divisions: 10,
min: 0.0,
max: 10.0,
value: sliderValue,
)
],
),
),
);
}
);
}
and everything is under State class of StatefullWidget.
Its look like doesn't update the value and when try to change the value keep in same position.
Update 1
The problem is there are 2 required parameters in Slider (onChanged, value), So I shoud update this or UI keep quite, see the video how the aplication is running
Video on Youtube
Update 2
I've also opened a issue to get help with this at Github repository, if someone wants to get more information can go to issue #19323
The problem is that it's not your dialog that holds the state. It's the widget that called showDialog. Same goes for when you call setState, you are calling in on the dialog creator.
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
Instead, you should make your dialog stateful. Hold the data inside that dialog. And then use Navigator.pop(context, sliderValue) to send the slider value back to the dialog creator.
The equivalent in your dialog would be
FlatButton(
onPressed: () => Navigator.of(context).pop(sliderValue),
child: Text("Hello"),
)
Which you can then catch inside the showDialog result :
final sliderValue = await showDialog<double>(
context: context,
builder: (context) => MyDialog(),
)
I've come up with the same issue with a checkbox and that's my solution, even if it's not the best approach. (see the comment in the code)
Future<Null>_showDialog() async {
return showDialog < Null > (
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return new AlertDialog(
title: Text("title"),
content: Container(
height: 150.0,
child: Checkbox(
value: globalSearch,
onChanged: (bool b) {
print(b);
globalSearch = b;
Navigator.of(context).pop(); // here I pop to avoid multiple Dialogs
_showDialog(); //here i call the same function
},
)),
);
},
);
}
Easiest and least amount of lines:
Use StatefulBuilder as top widget of Content in the AlertDialog.
StatefulBuilder(
builder: (context, state) => CupertinoSlider(
value: brightness,
onChanged: (val) {
state(() {
brightness = val;
});
},
),
));
I had similar issue and resolved by putting everything under AlertDialog in to a StatefullWidget.
class <your dialog widget> extends StatefulWidget {
#override
_FilterDialogState createState() => _FilterDialogState();
}
class _<your dialog widget> extends State<FilterDialog> {
#override
Widget build(BuildContext context) {
return AlertDialog(
//your alert dialog content here
);
}
}
create a statefull class with the slider at the return time and the double value should declare inside the statefull class thus the setstate func will work.
here is an example i done this for my slider popup its same for alert dialog use can declare the variable as global thus it can be accessed by other classes
class _PopupMenuState extends State<PopupMenu> {
double _fontSize=15.0;
#override
Widget build(BuildContext context) {
return Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
onChanged: (value) {
setState(() {
print(value);
_fontSize = value;
});
},
),
);
}
}