Save input values between widget rebuilds with Bloc Flutter - flutter

I have a form builded with Bloc package.
There are two options with textfields in it.
Switching between option i've made also with bloc and Visibility widget.
When I choose an option widget rebuilds, name and price values deletes.
What is the best way to save this values between choosing options?
Here is my Bloc code
class FormBloc extends Bloc<FormEvent, MyFormState> {
FormBloc() : super(MyFormState()) {
on<RadioButtonFormEvent>(_setRadioButtonState);
}
void _setRadioButtonState(
RadioButtonFormEvent event, Emitter<MyFormState> emit) async {
emit(RadioButtonFormState(
buttonIndex: event.buttonIndex,
buttonName: event.buttonName,
));
}
}
class MyFormState {}
class RadioButtonFormState extends MyFormState {
final int buttonIndex;
final String buttonName;
RadioButtonFormState({
required this.buttonIndex,
required this.buttonName,
});
}
abstract class FormEvent extends Equatable {}
class RadioButtonFormEvent extends FormEvent {
final int buttonIndex;
final String buttonName;
RadioButtonFormEvent({
required this.buttonIndex,
required this.buttonName,
});
#override
List<Object?> get props => [buttonIndex, buttonName,];
}
Here is Form code
class FormInput extends StatelessWidget {
const FormInput({super.key});
#override
Widget build(BuildContext context) {
final formBlocWatcher = context.watch<FormBloc>().state;
final nameController = TextEditingController();
final priceController = TextEditingController();
final formOneController = TextEditingController();
final formTwoController = TextEditingController();
final formThreeController = TextEditingController();
String formOptionController = '';
bool optionOneIsActive = true;
bool optionTwoIsActive = false;
if (formBlocWatcher is RadioButtonFormState) {
switch (formBlocWatcher.buttonIndex) {
case 0:
formOptionController = formBlocWatcher.buttonName;
break;
case 1:
optionTwoIsActive = true;
optionOneIsActive = false;
formOptionController = formBlocWatcher.buttonName;
break;
}
}
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 15,
left: 15,
right: 15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(hintText: 'Name'),
),
const SizedBox(height: 10),
TextField(
controller: priceController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Price'),
),
const SizedBox(height: 15),
OptionsWidget(),
Visibility(
visible: optionOneIsActive,
child: TextField(
controller: formOneController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form one'),
)),
Visibility(
visible: optionTwoIsActive,
child: Column(
children: [
TextField(
controller: formTwoController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form two'),
),
TextField(
controller: formThreeController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form three'),
),
],
)),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
BlocProvider.of<ProductsListBloc>(context).add(
AddProductEvent(
productName: nameController.text,
productPrice: int.parse(priceController.text),
productDescOne: formOneController.text,
productDescTwo: formTwoController.text,
productDescThree: formThreeController.text,
formOption: formOptionController,
),
);
},
child: Text('Create New'),
),
],
),
);
}
}
class OptionsWidget extends StatelessWidget {
OptionsWidget({super.key});
int value = 0;
Widget CustomRadioButton(String text, int index, BuildContext context) {
final formBloc = BlocProvider.of<FormBloc>(context);
final blocWatch = context.watch<FormBloc>().state;
if (blocWatch is RadioButtonFormState) {
value = blocWatch.buttonIndex;
}
return OutlinedButton(
onPressed: () {
formBloc.add(RadioButtonFormEvent(
buttonIndex: index,
buttonName: text,
));
},
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
side: BorderSide(color: (value == index) ? Colors.blue : Colors.grey),
),
child: Text(
text,
style: TextStyle(
color: (value == index) ? Colors.blue : Colors.grey,
),
));
}
#override
Widget build(BuildContext context) {
return Row(
children: [
CustomRadioButton("option one", 0, context),
const SizedBox(width: 15),
CustomRadioButton("option two", 1, context),
],
);
}
}

Your FormInput class should be extends from StatefulWidget, not StatelessWidget.
After this change, you should remove all TextEditingController assignments from the build() method and move them into initState().

Related

Search Result does not update instantly flutter

I'm emulating this search and filter github here and the codes are almost the same but the filtered results do not update instantly while I type and also I faced the following issues:
I will have to press enter on my laptop to finally get the filtered list
When I hit the close icon(which is to clear all the words), I will have to tap the searchbar again so that all my listtile are back on the listview.
Here's my code:
class _CurrencySelectState extends State<CurrencySelect> {
late List<Currency> resCur;
String query = '';
#override
void initState() {
super.initState();
resCur = currencyList;
}
void searchCur(String query) {
final List<Currency> filteredCur = currencyList.where((cur) {
final symbolLower = cur.symbol.toLowerCase(); // Search using symbol
final nameLower = cur.country.toLowerCase(); // Search using country
final searchLower = query.toLowerCase();
return symbolLower.contains(searchLower) ||
nameLower.contains(searchLower);
}).toList();
setState(() {
this.query = query;
resCur = filteredCur;
});
}
#override
Widget build(BuildContext context) {
Widget buildCur(Currency cur) => ListTile(
leading: Padding(
padding: EdgeInset.all(5)
child: SizedBox(
child: Column(
children: <Widget>[
SvgPicture.asset(
cur.assetPath,
),
]),
),
),
title: Column(
children: [
Text(
cur.symbol,
style: TextStyle(
...
),
Text(
cur.name,
style: TextStyle(
...
),
],
),
trailing: Text(
"0.25",
style: TextStyle(
...
),
);
return TextButton(
onPressed: () async {
showModalBottomSheet(
enableDrag: false,
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return DraggableScrollableSheet(
expand: false,
builder: (context, scrollController) {
return Column(
children: <Widget>[
SearchWidget(
text: query,
onChanged: searchCur,
hintText: "Enter symbol or country"
),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: resCur.length,
itemBuilder: (context, int index) {
final cur = resCur[index];
return buildCur(cur);
},
),
)
],
);
},
);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
...
),
SvgPicture.asset(
...
)
],
));
}
}
Searchwidget code:
import 'package:flutter/material.dart';
class SearchWidget extends StatefulWidget {
final String text;
final ValueChanged<String> onChanged;
final String hintText;
const SearchWidget({
Key? key,
required this.text,
required this.onChanged,
required this.hintText,
}) : super(key: key);
#override
_SearchWidgetState createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
final controller = TextEditingController();
#override
Widget build(BuildContext context) {
final styleActive = TextStyle(color: Colors.black);
final styleHint = TextStyle(color: Colors.black54);
final style = widget.text.isEmpty ? styleHint : styleActive;
return Container(
height: 42,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
border: Border.all(color: Colors.black26),
),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
controller: controller,
decoration: InputDecoration(
icon: Icon(Icons.search, color: style.color),
suffixIcon: widget.text.isNotEmpty
? GestureDetector(
child: Icon(Icons.close, color: style.color),
onTap: () {
controller.clear();
widget.onChanged('');
FocusScope.of(context).requestFocus(FocusNode());
},
)
: null,
hintText: widget.hintText,
hintStyle: style,
border: InputBorder.none,
),
style: style,
onChanged: widget.onChanged,
),
);
}
}

TextEditingController not passing text into named parameters

I am really struggling to understand why my code isn't working. I'm trying to pass the text from two controllers into another widget with named parameters which writes to Firebase.
My "Test" button properly prints both _titleController.text and _descriptionController.text
TextButton(
onPressed: (){
print(_titleController.text); //works fine
print(_descriptionController.text); //works fine
},
child: Text('test')
),
However when I pass these into my next widget it's blank! If I hardcore strings into these parameters it works properly:
PushNewE3 (
changeTitle: _titleController.text, //does not work (empty)
changeDescription: _descriptionController.text, //does not work (empty)
)
Full code:
class CreateE3 extends StatefulWidget {
const CreateE3({Key? key}) : super(key: key);
#override
_CreateE3State createState() => _CreateE3State();
}
class _CreateE3State extends State<CreateE3> {
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
#override
void initState(){
super.initState();
_titleController.addListener(_printLatestValue);
}
#override
void dispose(){
_titleController.dispose();
super.dispose();
}
void _printLatestValue(){
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('So Frustrating'),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 800,
child: Column(
children: [
Text('Originator: **Add Current User**') ,
TextField(
maxLength: 40,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Change Title'
),
controller: _titleController,
onEditingComplete: (){
//_title = _titleController.text;
},
),
Padding(
padding: const EdgeInsets.fromLTRB(0,10,0,0),
child: TextFormField(
maxLines: 5,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Detailed Description'
),
controller: _descriptionController,
),
),
TextButton(
onPressed: (){
print(_titleController.text); //successfully prints
print(_descriptionController.text); //successfully prints
},
child: Text('test')
),
PushNewE3 (
changeTitle: _titleController.text, //DOES NOT WORK (empty)
changeDescription: _descriptionController.text, //DOES NOT WORK (empty)
)
],
),
),
],
),
);
}
}
class PushNewE3 extends StatelessWidget {
final String changeTitle;
final String changeDescription;
PushNewE3({
required this.changeTitle,
required this.changeDescription
});
#override
Widget build(BuildContext context) {
// Create a CollectionReference called users that references the firestore collection
CollectionReference notificationsE3 = FirebaseFirestore.instance.collection('notificationsE3');
Future<void> pushNewE3() {
// Call the notifications CollectionReference to add a new E3 notification
return notificationsE3
.add({
//'originator': FirebaseAuth.instance.currentUser,
'changeTitle': changeTitle,
'changeDescription': changeDescription,
})
.then((value) => print("E3 Created"))
.catchError((error) => print("Failed to create E3: $error"));
}
return TextButton(
onPressed: (){
print('start:');
print(changeTitle);
print(changeDescription);
print('-end');
},
child: Text(
"Create E3",
),
);
}
}
EDIT:
I still don't understand why the above code doesn't work. I refactored my code into a single widget and now it's working. If anyone can explain why I would still appreciate understanding as there is clearly a gap in my knowledge.
If anyone in the future runs into the same problem here is the refactored code:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'main.dart';
var global = 'blank';
class CreateE3 extends StatefulWidget {
const CreateE3({Key? key}) : super(key: key);
#override
_CreateE3State createState() => _CreateE3State();
}
class _CreateE3State extends State<CreateE3> {
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
#override
Widget build(BuildContext context) {
// Create a CollectionReference called users that references the firestore collection
CollectionReference notificationsE3 = FirebaseFirestore.instance.collection('notificationsE3');
Future<void> pushNewE3() {
// Call the notifications CollectionReference to add a new E3 notification
return notificationsE3
.add({
//'originator': FirebaseAuth.instance.currentUser,
'changeTitle': _titleController.text,
'changeDescription': _descriptionController.text,
})
.then((value) => print("E3 Created"))
.catchError((error) => print("Failed to create E3: $error"));
}
return Scaffold(
appBar: AppBar(
title: Text(_titleController.text),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 800,
child: Column(
children: [
Text('Originator: **Add Current User**') ,
TextField(
maxLength: 40,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Change Title'
),
controller: _titleController,
onChanged: (text){
setState(() {
});
},
),
Padding(
padding: const EdgeInsets.fromLTRB(0,10,0,0),
child: TextFormField(
maxLines: 5,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Detailed Description'
),
controller: _descriptionController,
),
),
TextButton(
onPressed: (){
pushNewE3();
},
child: Text('SAVE')
),
],
),
),
],
),
);
}
}
in the onPressed To pass a value and show it, you have to use setState(() { _myState = newValue; });
Something like this
TextButton(
onPressed: (){
print(_titleController.text);
print(_descriptionController.text);
setState(() { _myNewText = _titleController.text; });
},
child: Text('test')
),
I'm not sure what are you trying to do exactly but here's what I did:
1 - add a local variable _title
2 - add this code to the onPressed function:
setState(() {
_title= _titleController.text;
});
This is the whole code :
class CreateE3 extends StatefulWidget {
const CreateE3({Key? key}) : super(key: key);
#override
_CreateE3State createState() => _CreateE3State();
}
class _CreateE3State extends State<CreateE3> {
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
String _title = 'So Frustrating';
#override
void initState(){
super.initState();
}
#override
void dispose(){
_titleController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 400,
child: Column(
children: [
Text('Originator: **Add Current User**') ,
TextField(
maxLength: 40,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Change Title'
),
controller: _titleController,
onEditingComplete: (){
//_title = _titleController.text;
},
),
Padding(
padding: const EdgeInsets.fromLTRB(0,10,0,0),
child: TextFormField(
maxLines: 5,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Detailed Description'
),
controller: _descriptionController,
),
),
TextButton(
onPressed: (){
print(_titleController.text);
print(_descriptionController.text);
setState(() {
_title = _titleController.text;
});
},
child: Text('test')
),
],
),
),
],
),
);
}
}
.........................
so this is when you first start the app :
after changing the TextField and pressing the 'test button the title in the appbar change :

How do I validate dynamically created forms in flutter?

I'm currently dynamically creating a custom form widget (Row) and am wondering what the best way to validate each form was. By attempting to use a global FormState key, nothing works as the key is shared by each form instance (I assume).
This creates issues such as the keyboard immediately dropping out upon focusing on a textfield.
Was wondering if anyone has a solution to this or could point me in the right direction. Thanks!
Root Widget:
class ExerciseTable extends ConsumerWidget {
final Exercise exercise;
ExerciseTable({#required this.exercise});
#override
Widget build(BuildContext context, ScopedReader watch) {
final _exerciseTableController = watch(exerciseTableControllerProvider);
/*
* Logic of where I build the form rows dynamically
*/
List<Widget> _buildFormRows() {
List<Widget> rows = [];
int sets = int.parse(exercise.sets);
for (int i = 1; i < sets; i++) {
rows.add(
_BuildExerciseRow(
set: i.toString(),
),
);
}
return rows;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
exercise.exerciseName,
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(height: 8),
YoutubePlayerTile(
url: exercise.exerciseURL,
),
const SizedBox(height: 8),
_BuildRowHeader(),
Column(children: _buildFormRows())
],
);
}
}
Individual Form Row Widget:
/**
* * Form Rows
*/
class _BuildExerciseRow extends StatelessWidget {
final String set;
final _formKey = GlobalKey<FormState>();
final _kgTextEditingController = TextEditingController();
final _repsTextEditingController = TextEditingController();
_BuildExerciseRow({this.set});
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 5),
child: Form(
key: _formKey,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Text(set),
const SizedBox(width: 15),
Text("-"),
const SizedBox(width: 15),
_BuildInputTextField(
controller: _kgTextEditingController,
validator: (value) {
if (value.isEmpty) {
return "Please enter some text";
} else
return "";
}),
// const SizedBox(width: 15),
_BuildInputTextField(
controller: _repsTextEditingController,
),
TickBox(onTap: () => _formKey.currentState.validate())
],
),
),
);
}
}
class _BuildInputTextField extends StatelessWidget {
// final int keyValue;
final String Function(String) validator;
final TextEditingController controller;
_BuildInputTextField({this.validator, #required this.controller});
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 2),
height: 39,
width: 80,
child: TextFormField(
validator: validator,
inputFormatters: [
LengthLimitingTextInputFormatter(6),
],
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(bottom: 5, left: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(width: 1),
),
),
onChanged: (value) => {}),
);
}
}
Custom TextFormField Widget:
class _BuildInputTextField extends StatelessWidget {
// final int keyValue;
final String Function(String) validator;
final TextEditingController controller;
_BuildInputTextField({this.validator, #required this.controller});
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 2),
height: 39,
width: 80,
child: TextFormField(
validator: validator,
inputFormatters: [
LengthLimitingTextInputFormatter(6),
],
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(bottom: 5, left: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(width: 1),
),
),
onChanged: (value) => {}),
);
}
}
try changing your validator callback as
validator: (value) {
if (value.isEmpty) {
return "Please enter some text";
} else
return null;
}),
Fixed!
It was super simple, I forgot to convert _BuildExerciseRow to a Stateful Widget. (In my case, a Consumer Widget from Riverpod which extends StatefulWidget).

TextFromField is losing value after sate changed

TextFromField is losing its value when the state change.
Here is the full code https://github.com/imSaharukh/cgpa_final.git
How can I fix that?
Check this GIF
The problem is how you create your TextEditingControllers. Everytime the build method is called new TextEditingControllers are created.
What you want to do is create 3 TextEditingController variables at the top inside _MyHomePageState class. (Also no need to use the new keyword in dart).
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
TextEditingController nameController = TextEditingController();
TextEditingController cgpaController = TextEditingController();
TextEditingController crController = TextEditingController();
and pass these to your CustomCard
child: CustomCard(
key: UniqueKey(),
index: index,
cgpa: cgpa,
namecontroller: nameController,
cgpacontroller: cgpaController,
crcontroller: crController),
Hope this helps
EDIT:
I don't know how to create a pull request but I made some changes for you and tested it on an iOS sim.
What i did:
Renamed Details to Course
Converted CusomCard into an statefull widget
Only a Course object is now passed to CustomCard
The dismissable now gets a key based on the course.
Moved the controllers to CustomCard
Modified some code in CGPA to make it all work
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Column(
children: [
Expanded(
child: Consumer<CGPA>(builder: (context, cgpa, _) {
return Form(
key: _formKey,
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: cgpa.courses.length,
itemBuilder: (BuildContext context, int index) {
return Dismissible(
key: Key(cgpa.getKeyValue(index)),
onDismissed: (direction) {
cgpa.remove(index);
print(cgpa.courses.length);
},
child: CustomCard(
course: cgpa.getCourse(index),
),
);
},
),
);
}),
),
],
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.all(15.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Provider.of<CGPA>(context, listen: false).add();
// print(cgpa.details.length);
// cgpa.details[indexs] = Details();
},
),
),
),
],
),
),
floatingActionButton: OutlineButton(
onPressed: () {
// for (var item in cgpa.details) {
// print(item.credit);
// }
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text("calculate"),
),
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(30.0),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
CustomCard
class CustomCard extends StatefulWidget {
CustomCard({#required this.course});
final Course course;
#override
_CustomCardState createState() => _CustomCardState();
}
class _CustomCardState extends State<CustomCard> {
TextEditingController nameController;
TextEditingController cgpaController;
TextEditingController crController;
#override
void initState() {
super.initState();
nameController = TextEditingController(text: widget.course.name);
cgpaController = TextEditingController(
text: widget.course.gpa == null ? "" : widget.course.gpa.toString());
crController = TextEditingController(
text: widget.course.credit == null
? ""
: widget.course.credit.toString());
}
#override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 3,
child: TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: "COURSE NAME"),
onChanged: (value) {
widget.course.name = value;
},
),
),
SizedBox(
width: 10,
),
Expanded(
child: TextFormField(
controller: cgpaController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: "GPA"),
onChanged: (value) {
//print(value);
widget.course.gpa = double.parse(value);
},
validator: (value) {
if (double.parse(value) > 4 && double.parse(value) < 0) {
return 'can\'t more then 4';
}
return null;
},
),
),
SizedBox(
width: 10,
),
Expanded(
child: TextFormField(
controller: crController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: "CREDIT"),
onChanged: (value) {
widget.course.credit = double.parse(value);
},
validator: (value) {
if (value.isEmpty) {
return 'can\'t be empty';
}
return null;
},
),
),
],
),
),
);
}
}
CGPA
class CGPA with ChangeNotifier {
Map<int, Course> courses = new Map();
var index = 0;
add() {
courses[index] = Course();
index++;
notifyListeners();
}
remove(int listIndex) {
courses.remove(courses.keys.toList()[listIndex]);
notifyListeners();
}
String getKeyValue(int listIndex) => courses.keys.toList()[listIndex].toString();
Course getCourse(int listIndex) => courses.values.toList()[listIndex];
}
class Course {
Course({this.credit, this.gpa, this.name});
String name;
double credit;
double gpa;
#override
String toString() {
return 'Course{name: $name, credit: $credit, gpa: $gpa}';
}
}

Is there a number input field in flutter with increment/decrement buttons attached to the field?

I am trying to create a number input field with and up and down arrow button to increment and decrement its value. I am wondering if there is any inbuilt widget which already provides this functionality. I have to create couple of such fields in my UI and creating so many stateful widgets makes me wonder if there is any simpler approach.
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Increment Decrement Demo';
return MaterialApp(
title: title,
home: NumberInputWithIncrementDecrement(),
);
}
}
class NumberInputWithIncrementDecrement extends StatefulWidget {
#override
_NumberInputWithIncrementDecrementState createState() =>
_NumberInputWithIncrementDecrementState();
}
class _NumberInputWithIncrementDecrementState
extends State<NumberInputWithIncrementDecrement> {
TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.text = "0"; // Setting the initial value for the field.
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Field increment decrement'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.numberWithOptions(
decimal: false, signed: false),
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter.digitsOnly
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MaterialButton(
minWidth: 5.0,
child: Icon(Icons.arrow_drop_up),
onPressed: () {
int currentValue = int.parse(_controller.text);
setState(() {
currentValue++;
_controller.text =
(currentValue).toString(); // incrementing value
});
},
),
MaterialButton(
minWidth: 5.0,
child: Icon(Icons.arrow_drop_down),
onPressed: () {
int currentValue = int.parse(_controller.text);
setState(() {
print("Setting state");
currentValue--;
_controller.text =
(currentValue).toString(); // decrementing value
});
},
),
],
),
Spacer(
flex: 2,
)
],
),
),
);
}
}
current output looks some thing like this.
I am looking for something like the following in a simpler manner like in HTML number input field.
I have laid out my Number input widget as shown below. I think I will go ahead with this approach until someone has any different idea for the same.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Increment Decrement Demo';
return MaterialApp(
title: title,
home: NumberInputWithIncrementDecrement(),
);
}
}
class NumberInputWithIncrementDecrement extends StatefulWidget {
#override
_NumberInputWithIncrementDecrementState createState() =>
_NumberInputWithIncrementDecrementState();
}
class _NumberInputWithIncrementDecrementState
extends State<NumberInputWithIncrementDecrement> {
TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.text = "0"; // Setting the initial value for the field.
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Field increment decrement'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Container(
width: 60.0,
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Colors.blueGrey,
width: 2.0,
),
),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: TextFormField(
textAlign: TextAlign.center,
decoration: InputDecoration(
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
controller: _controller,
keyboardType: TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter.digitsOnly
],
),
),
Container(
height: 38.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
),
),
),
child: InkWell(
child: Icon(
Icons.arrow_drop_up,
size: 18.0,
),
onTap: () {
int currentValue = int.parse(_controller.text);
setState(() {
currentValue++;
_controller.text = (currentValue)
.toString(); // incrementing value
});
},
),
),
InkWell(
child: Icon(
Icons.arrow_drop_down,
size: 18.0,
),
onTap: () {
int currentValue = int.parse(_controller.text);
setState(() {
print("Setting state");
currentValue--;
_controller.text =
(currentValue > 0 ? currentValue : 0)
.toString(); // decrementing value
});
},
),
],
),
),
],
),
),
),
),
);
}
}
Update:
As I see many of us like this approach I created a package for the same. Maybe its helpful for some of us.
number_inc_dec
I was looking for a simple -/+ step counter, so I made one... don't pretend too much, I'm using flutter since a couple of days :-)
It has a maximum and minimum value, by default the minimum is set to zero and maximum to 10, but if you need negative values, just set it to -N.
Preview
Widget source
import 'package:flutter/material.dart';
class NumericStepButton extends StatefulWidget {
final int minValue;
final int maxValue;
final ValueChanged<int> onChanged;
NumericStepButton(
{Key key, this.minValue = 0, this.maxValue = 10, this.onChanged})
: super(key: key);
#override
State<NumericStepButton> createState() {
return _NumericStepButtonState();
}
}
class _NumericStepButtonState extends State<NumericStepButton> {
int counter= 0;
#override
Widget build(BuildContext context) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: Icon(
Icons.remove,
color: Theme.of(context).accentColor,
),
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 18.0),
iconSize: 32.0,
color: Theme.of(context).primaryColor,
onPressed: () {
setState(() {
if (counter > widget.minValue) {
counter--;
}
widget.onChanged(counter);
});
},
),
Text(
'$counter',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black87,
fontSize: 18.0,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).accentColor,
),
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 18.0),
iconSize: 32.0,
color: Theme.of(context).primaryColor,
onPressed: () {
setState(() {
if (counter < widget.maxValue) {
counter++;
}
widget.onChanged(counter);
});
},
),
],
),
);
}
}
Read the counter value
...
int yourLocalVariable = 0;
...
return Container(
child: NumericStepButton(
maxValue: 20,
onChanged: (value) {
yourLocalVariable = value;
},
),
)
],
...
Happy coding!
This is the most complete solution you can find
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class NumberTextField extends StatefulWidget {
final TextEditingController? controller;
final FocusNode? focusNode;
final int min;
final int max;
final int step;
final double arrowsWidth;
final double arrowsHeight;
final EdgeInsets contentPadding;
final double borderWidth;
final ValueChanged<int?>? onChanged;
const NumberTextField({
Key? key,
this.controller,
this.focusNode,
this.min = 0,
this.max = 999,
this.step = 1,
this.arrowsWidth = 24,
this.arrowsHeight = kMinInteractiveDimension,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
this.borderWidth = 2,
this.onChanged,
}) : super(key: key);
#override
State<StatefulWidget> createState() => _NumberTextFieldState();
}
class _NumberTextFieldState extends State<NumberTextField> {
late TextEditingController _controller;
late FocusNode _focusNode;
bool _canGoUp = false;
bool _canGoDown = false;
#override
void initState() {
super.initState();
_controller = widget.controller ?? TextEditingController();
_focusNode = widget.focusNode ?? FocusNode();
_updateArrows(int.tryParse(_controller.text));
}
#override
void didUpdateWidget(covariant NumberTextField oldWidget) {
super.didUpdateWidget(oldWidget);
_controller = widget.controller ?? _controller;
_focusNode = widget.focusNode ?? _focusNode;
_updateArrows(int.tryParse(_controller.text));
}
#override
Widget build(BuildContext context) => TextField(
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
decoration: InputDecoration(
counterText: '',
isDense: true,
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
contentPadding: widget.contentPadding.copyWith(right: 0),
suffixIconConstraints: BoxConstraints(
maxHeight: widget.arrowsHeight, maxWidth: widget.arrowsWidth + widget.contentPadding.right),
suffixIcon: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(widget.borderWidth), bottomRight: Radius.circular(widget.borderWidth))),
clipBehavior: Clip.antiAlias,
alignment: Alignment.centerRight,
margin: EdgeInsets.only(
top: widget.borderWidth,
right: widget.borderWidth,
bottom: widget.borderWidth,
left: widget.contentPadding.right),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Opacity(opacity: _canGoUp ? 1 : .5, child: const Icon(Icons.arrow_drop_up)),
onTap: _canGoUp ? () => _update(true) : null))),
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Opacity(opacity: _canGoDown ? 1 : .5, child: const Icon(Icons.arrow_drop_down)),
onTap: _canGoDown ? () => _update(false) : null))),
]))),
maxLines: 1,
onChanged: (value) {
final intValue = int.tryParse(value);
widget.onChanged?.call(intValue);
_updateArrows(intValue);
},
inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]);
void _update(bool up) {
var intValue = int.tryParse(_controller.text);
intValue == null ? intValue = 0 : intValue += up ? widget.step : -widget.step;
_controller.text = intValue.toString();
_updateArrows(intValue);
_focusNode.requestFocus();
}
void _updateArrows(int? value) {
final canGoUp = value == null || value < widget.max;
final canGoDown = value == null || value > widget.min;
if (_canGoUp != canGoUp || _canGoDown != canGoDown)
setState(() {
_canGoUp = canGoUp;
_canGoDown = canGoDown;
});
}
}
class _NumberTextInputFormatter extends TextInputFormatter {
final int min;
final int max;
_NumberTextInputFormatter(this.min, this.max);
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
if (const ['-', ''].contains(newValue.text)) return newValue;
final intValue = int.tryParse(newValue.text);
if (intValue == null) return oldValue;
if (intValue < min) return newValue.copyWith(text: min.toString());
if (intValue > max) return newValue.copyWith(text: max.toString());
return newValue.copyWith(text: intValue.toString());
}
}
Simple, BLoC-friendly approach:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = TextEditingController();
final _streamController = StreamController<int>();
Stream<int> get _stream => _streamController.stream;
Sink<int> get _sink => _streamController.sink;
int initValue = 1;
#override
void initState() {
_sink.add(initValue);
_stream.listen((event) => _controller.text = event.toString());
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: [
TextButton(
onPressed: () {
_sink.add(--initValue);
},
child: Icon(Icons.remove)),
Container(
width: 50,
child: TextField(
controller: _controller,
),
),
TextButton(
onPressed: () {
_sink.add(++initValue);
},
child: Icon(Icons.add)),
],
)
],
),
),
);
}
#override
void dispose() {
_streamController.close();
_controller.dispose();
super.dispose();
}
}