Related
How do you achieve the smooth transition when the checkmark is added?
Clicking the element will setState update the pressAttention variable, and therefore add the checkmark widget to the list of children of the row.
For now it just instantly rebuilds the row, and adds the checkmark, but I would really like it to smoothly do as in the GIF.
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.amount,
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.w700),
),
if (pressAttention)
Padding(
padding: const EdgeInsets.only(left: 10),
child: Container(
width: 23,
height: 23,
decoration: BoxDecoration(
color: Theme.of(context).highlightColor,
shape: BoxShape.circle,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: SvgPicture.asset(
MyIcons.checkmarkThick,
),
),
),
)
],
),
try this:
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> with TickerProviderStateMixin {
late Animation<double> _animation;
late Animation<double> _opacityAnimation;
late Animation _colorAnimation;
late AnimationController _controller;
var pressAttention = false;
#override
void initState() {
super.initState();
_controller =
AnimationController(duration: Duration(milliseconds: 500), vsync: this)
..addListener(() => setState(() {}));
_animation = Tween(begin: 15.0, end: 0.0).animate(_controller);
_opacityAnimation = Tween(begin: 0.0, end: 1.0).animate(_controller);
_colorAnimation =
ColorTween(begin: Colors.grey, end: Colors.purple).animate(_controller);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Center(
child: Container(
child: InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onTap: () {
if (pressAttention) {
_controller.forward();
} else {
_controller.reverse();
}
setState(() {
pressAttention = !pressAttention;
});
},
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: 100,
decoration: BoxDecoration(
border: Border.all(color: _colorAnimation.value),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Transform.translate(
offset: Offset(_animation.value, 0),
child: Text(
'1000',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700),
),
),
Opacity(
opacity: _opacityAnimation.value,
child: Padding(
padding: EdgeInsets.only(left: 10),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).highlightColor,
shape: BoxShape.circle,
),
child: const Padding(
padding: EdgeInsets.all(4),
child: Icon(
Icons.check,
size: 13,
),
),
),
),
)
],
),
);
}),
),
),
),
),
);
}
}
You can achieve this with a combination of Animated Widgets. Set their behaviours and durations and you should be good to go.
I am implementing AnimatedList on my app with several TextEditingControllers. I would like to dynamically update, insert and remove data. I've read this question and an article on how to update data in an AnimatedList and this is how my code looks:
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _HomePage();
}
class _HomePage extends State<HomePage> {
List<Board> boards = [Board.empty()];
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
void Function()? removeItemCallback(int index) {
if (boards.length <= 1) {
return null;
}
return (() {
FocusManager.instance.primaryFocus?.unfocus();
final removedBoard = boards.removeAt(index);
listKey.currentState!.removeItem(
index,
(context, animation) => SizeTransition(
axis: Axis.vertical,
sizeFactor: animation,
child: BoardListItem(
board: removedBoard,
index: index,
removeItemCallback: removeItemCallback(index),
),
),
duration: const Duration(milliseconds: 500));
setState(() {});
});
}
#override
Widget build(BuildContext context) {
return AnimatedList(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
key: listKey,
initialItemCount: boards.length,
itemBuilder: (context, index, animation) {
return SizeTransition(
axis: Axis.vertical,
sizeFactor: animation,
child: BoardListItem(
board: boards[index],
index: index,
removeItemCallback: removeItemCallback(index)));
},
);
}
}
class BoardListItem extends StatefulWidget {
const BoardListItem(
{Key? key,
required this.board,
required this.index,
required this.removeItemCallback})
: super(key: key);
final Board board;
final int index;
final void Function()? removeItemCallback;
#override
State<StatefulWidget> createState() => _BoardListItem();
}
class _BoardListItem extends State<BoardListItem> {
late final TextEditingController bigStakeController;
late final TextEditingController smallStakeController;
late final TextEditingController numberController;
#override
void initState() {
super.initState();
print('initing state');
bigStakeController =
TextEditingController(text: widget.board.bigStake.toString());
smallStakeController =
TextEditingController(text: widget.board.smallStake.toString());
numberController =
TextEditingController(text: widget.board.number.toString());
bigStakeController.addListener(() {
if (bigStakeController.text.isNotEmpty) {
widget.board.bigStake = int.parse(bigStakeController.text);
} else {
widget.board.bigStake = 0;
}
});
smallStakeController.addListener(() {
if (smallStakeController.text.isNotEmpty) {
widget.board.smallStake = int.parse(smallStakeController.text);
} else {
widget.board.smallStake = 0;
}
});
}
#override
void dispose() {
print('disposing these');
bigStakeController.dispose();
smallStakeController.dispose();
numberController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.grey, width: 1.5)),
child: Column(children: [
Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.5, color: Colors.grey))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: (() {
numberController.text = '1234';
}),
icon: const Icon(Icons.shuffle)),
Text(
'Board ${widget.index + 1}',
style: const TextStyle(fontWeight: FontWeight.w500),
),
IconButton(
onPressed: widget.removeItemCallback,
icon: const Icon(Icons.delete_outline))
],
),
),
Container(
margin: const EdgeInsets.only(top: 15),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.5, color: Colors.grey))),
child: PinCodeTextField(
controller: numberController,
autoDisposeControllers: false,
autoUnfocus: false,
length: 4,
showCursor: true,
enablePinAutofill: false,
keyboardType: TextInputType.number,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
textStyle: const TextStyle(
fontSize: 20, fontWeight: FontWeight.w500),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]'))
],
animationType: AnimationType.scale,
//errorAnimationController: errorController,
appContext: context,
pinTheme: PinTheme(
shape: PinCodeFieldShape.box,
borderRadius: BorderRadius.circular(5),
fieldHeight: 50,
fieldWidth: 40,
selectedColor: const Color(0xFFB666D2),
inactiveColor: Colors.grey.shade400,
activeColor: const Color(0xFFB666D2)),
onChanged: (value) {
widget.board.number = value;
},
beforeTextPaste: (text) => false,
)),
IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.3,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 5), //To manage vertical divider height
child: TextFormField(
controller: bigStakeController,
onTap: () {
if (widget.board.bigStake == 0) {
bigStakeController.clear();
}
},
onEditingComplete: () {
if (widget.board.bigStake == 0) {
bigStakeController.text = '0';
}
FocusManager.instance.primaryFocus?.unfocus();
},
enableInteractiveSelection: false,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]'))
],
keyboardType: TextInputType.number,
decoration: const InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
border: InputBorder.none,
isDense: true,
label: Text('Big'),
),
)),
),
const VerticalDivider(
thickness: 1.5,
color: Colors.grey,
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.3,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: TextFormField(
controller: smallStakeController,
onTap: () {
if (widget.board.smallStake == 0) {
smallStakeController.clear();
}
},
onEditingComplete: () {
if (widget.board.smallStake == 0) {
smallStakeController.text = '0';
}
FocusManager.instance.primaryFocus?.unfocus();
},
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]'))
],
keyboardType: TextInputType.number,
enableInteractiveSelection: false,
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
floatingLabelBehavior: FloatingLabelBehavior.always,
label: Text('Small'),
),
),
),
)
],
),
),
])),
);
}
}
When removing the first item, an unexpected behavior happens where the controller's text is sent to the second item. This does not happen when you remove the second item. (e.g Removing board 2).
Upon checking my data source for my second item, nothing was changed too.
Am I implementing TextEditingControllers into an AnimatedList wrongly? And if so, how do I properly implement it?
How can i show text fields based on user inputs when the button is pressed?
You can find in the screen shot i'm trying to do.
here is the code:
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textController = TextEditingController();
void showTextFields(){
TextField();
}
return Scaffold(
appBar: AppBar(
),
body: Center(
child: Column(
children: [
SizedBox(height: 50,),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20.0),
child: TextField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text fields number'
),
),
),
SizedBox(height: 10,),
ElevatedButton(
onPressed: showTextFields,
child: Text('Press to show text field')
),
],
),
),
);
}
}
I hope my help will be of use to you, greetings
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey _parentKey = GlobalKey();
int countItems = 0;
late TextEditingController textController;
#override
void initState() {
super.initState();
textController = TextEditingController();
}
#override
Widget build(BuildContext context) {
void showTextFields() {
setState(() {
countItems = int.parse(textController.text);
});
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
const SizedBox(
height: 50,
),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20.0),
child: TextField(
controller: textController,
decoration:
const InputDecoration(labelText: 'Text fields number'),
),
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: showTextFields,
child: const Text('Press to show text field')),
const SizedBox(
height: 10,
),
(countItems > 0)
? Flexible(
child: ListView.builder(
itemCount: countItems,
itemBuilder: (BuildContext context, int index) {
return TextField(
style: TextStyle(color: Colors.white),
cursorColor: Colors.white,
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: "TextField " + index.toString(),
hintStyle: TextStyle(color: Colors.grey[400]),
),
);
}),
)
: const SizedBox()
],
),
),
);
}
}
You can this code, Firstly you must to change stateless to StatefullWidget
and then examine to blow code
class ShowTextFieldValue extends StatefulWidget {
const ShowTextFieldValue({ Key? key }) : super(key: key);
#override
_ShowTextFieldValueState createState() => _ShowTextFieldValueState();
}
class _ShowTextFieldValueState extends State<ShowTextFieldValue> {
#override
Widget build(BuildContext context) {
final textController = TextEditingController();
String value = "";
return Scaffold(
appBar: AppBar(
),
body: Center(
child: Column(
children: [
SizedBox(height: 50,),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20.0),
child: TextField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text fields number'
),
),
),
SizedBox(height: 200,width: 200,
child: Center( child: Text(value)),
),
ElevatedButton(
onPressed: (){
setState(() {
value= textController.text;
});
},
child: Text('Press to show text field')
),
],
),
),
);
}
}
I am struggling with this Chat Screen. The app is meant to ask questions (not part of the below code) and the user either selects answers or types them. When the user types a first answer everything goes according to the plan and a first message is displayed. However the app then goes on displaying the second answer twice, the third one three times and so on.
I have been facing this issue for a few days and I cannot figure out why the app behaves the way it does. Could you please take a look at the code and suggest a way to fix this?
To give you some background information, this Chat Screen is part of a larger application. It should subscribe to a stream when the user opens the app. Then each message is pushed to the stream, whether it is a question asked by the bot or an answer given by the User. The system listens to the stream and displays a new message each time the stream broadcasts something, in our case the latest user input.
I am using a list of message models built from the stream to display the messages. For the purpose of asking this question I simplified the model to the extreme but in practice it has 23 fields. Creating this list of messages is the best solution I managed to think of but there may be a better way to handle this situation. Feel free to let me know if you know of any.
Here is the code that I am running.
import 'package:flutter/material.dart';
import 'dart:async';
StreamController<ChatMessageModel> _chatMessagesStreamController = StreamController<ChatMessageModel>.broadcast();
Stream _chatMessagesStream = _chatMessagesStreamController.stream;
const Color primaryColor = Color(0xff6DA7B9);
const Color secondaryColor = Color(0xffF0F0F0);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chat Screen',
home: ChatScreen(),
);
}
}
class ChatMessageModel {
final String message;
const ChatMessageModel({
this.message,
}
);
factory ChatMessageModel.turnSnapshotIntoListRecord(Map data) {
return ChatMessageModel(
message: data['message'],
);
}
#override
List<Object> get props => [
message,
];
}
class ChatScreen extends StatefulWidget {
static const String id = 'chat_screen9';
#override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _messageTextController = TextEditingController();
String _userInput;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: secondaryColor,
appBar: AppBar(
title: Row(
children: [
Container(
padding: EdgeInsets.all(8.0),
child: Text('Chat Screen',
style: TextStyle(color: Colors.white,),
),
)
],
),
backgroundColor: primaryColor,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessagesStream(),
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: primaryColor,
width: 1.0,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
controller: _messageTextController,
onChanged: (value) {
_userInput = value;
},
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
hintText: 'Type your answer here',
// border: InputBorder.none,
),
),
),
TextButton(
onPressed: () {
_messageTextController.clear();
debugPrint('Adding a ChatMessageModel with the message $_userInput to the Stream');
ChatMessageModel chatMessageModelRecord = ChatMessageModel(message: _userInput);
_chatMessagesStreamController.add(chatMessageModelRecord,);
},
child: Text(
'OK',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
],
),
),
],
),
),
);
}
}
class MessagesStream extends StatelessWidget {
List<ChatMessageModel> _allMessagesContainedInTheStream = [];
#override
Widget build(BuildContext context) {
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot) {
_chatMessagesStream.listen((streamedMessages) {
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
}
);
return Expanded(
child: ListView.builder(
// reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index) {
if (snapshot.hasData) {
return UserChatBubble(chatMessageModelRecord: _allMessagesContainedInTheStream[index]);
}
},
),
);
},
);
}
}
class UserChatBubble extends StatelessWidget {
final ChatMessageModel chatMessageModelRecord;
const UserChatBubble({
Key key,
#required this.chatMessageModelRecord,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 5,),
child: Container(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 7 / 10,),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
topLeft: Radius.circular(15.0),
),
color: primaryColor,
),
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 20,),
child: Text(chatMessageModelRecord.message,
style: TextStyle(
fontSize: 17,
// fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
);
}
}
First of all, thank you for the interesting problem and functioning example provided. I had to do some small changes to convert it to "null-safety", but my code should work on your computer too.
The only problem you had initialization of _chatMessagesStream listener. You should do it only once and ideally in initState, to call it only once.
So here is the fix for you:
class MessagesStream extends StatefulWidget {
#override
_MessagesStreamState createState() => _MessagesStreamState();
}
class _MessagesStreamState extends State<MessagesStream> {
final List<ChatMessageModel> _allMessagesContainedInTheStream = [];
#override
void initState() {
_chatMessagesStream.listen((streamedMessages) {
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
});
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot) {
return Expanded(
child: ListView.builder(
// reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index) {
if (snapshot.hasData) {
return UserChatBubble(
chatMessageModelRecord:
_allMessagesContainedInTheStream[index],
);
} else {
print(snapshot.connectionState);
return Container();
}
},
),
);
},
);
}
}
Also providing full code for null-safety just in case!
import 'package:flutter/material.dart';
import 'dart:async';
final StreamController<ChatMessageModel> _chatMessagesStreamController =
StreamController<ChatMessageModel>.broadcast();
final Stream<ChatMessageModel> _chatMessagesStream =
_chatMessagesStreamController.stream;
const Color primaryColor = Color(0xff6DA7B9);
const Color secondaryColor = Color(0xffF0F0F0);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chat Screen',
home: ChatScreen(),
);
}
}
class ChatMessageModel {
final String? message;
const ChatMessageModel({
this.message,
});
factory ChatMessageModel.turnSnapshotIntoListRecord(Map data) {
return ChatMessageModel(
message: data['message'],
);
}
List<Object> get props => [
message!,
];
}
class ChatScreen extends StatefulWidget {
static const String id = 'chat_screen9';
#override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _messageTextController = TextEditingController();
String? _userInput;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: secondaryColor,
appBar: AppBar(
title: Row(
children: [
Container(
padding: EdgeInsets.all(8.0),
child: Text(
'Chat Screen',
style: TextStyle(
color: Colors.white,
),
),
)
],
),
backgroundColor: primaryColor,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessagesStream(),
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: primaryColor,
width: 1.0,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
controller: _messageTextController,
onChanged: (value) {
_userInput = value;
},
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
vertical: 10.0, horizontal: 20.0),
hintText: 'Type your answer here',
// border: InputBorder.none,
),
),
),
TextButton(
onPressed: () {
_messageTextController.clear();
debugPrint(
'Adding a ChatMessageModel with the message $_userInput to the Stream');
ChatMessageModel chatMessageModelRecord =
ChatMessageModel(message: _userInput);
_chatMessagesStreamController.add(
chatMessageModelRecord,
);
},
child: Text(
'OK',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
],
),
),
],
),
),
);
}
}
class MessagesStream extends StatefulWidget {
#override
_MessagesStreamState createState() => _MessagesStreamState();
}
class _MessagesStreamState extends State<MessagesStream> {
final List<ChatMessageModel> _allMessagesContainedInTheStream = [];
#override
void initState() {
_chatMessagesStream.listen((streamedMessages) {
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
});
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot) {
return Expanded(
child: ListView.builder(
// reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index) {
if (snapshot.hasData) {
return UserChatBubble(
chatMessageModelRecord:
_allMessagesContainedInTheStream[index],
);
} else {
print(snapshot.connectionState);
return Container();
}
},
),
);
},
);
}
}
class UserChatBubble extends StatelessWidget {
final ChatMessageModel chatMessageModelRecord;
const UserChatBubble({
Key? key,
required this.chatMessageModelRecord,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 5,
),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 7 / 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
topLeft: Radius.circular(15.0),
),
color: primaryColor,
),
padding: EdgeInsets.symmetric(
vertical: 8,
horizontal: 20,
),
child: Text(
"${chatMessageModelRecord.message}",
style: TextStyle(
fontSize: 17,
// fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
);
}
}
I am creating a simple grocery list creator in Flutter. I am trying to go about this by having a plus button that will add ingredient text fields when you press it. Here is what I have done:
body: Container(
padding: EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 30.0),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Text(
'Ingredients ',
style: GoogleFonts.biryani(fontSize: 15.0)),
IconButton(
icon: new Icon(Icons.add),
onPressed: () {
setState(() {
countings++;
});
debugPrint('$countings');
},
)
],
),
SizedBox(height: 10.0),
ListOfIngsWidget(countings, key: UniqueKey())
],
),
)
And here is the ListOfIngsWidget:
class ListOfIngsWidget extends StatefulWidget {
final int countIngs;
const ListOfIngsWidget(this.countIngs, {Key key}) : super(key: key);
#override
_ListOfIngsState createState() => _ListOfIngsState();
}
class _ListOfIngsState extends State<ListOfIngsWidget> {
List<TextEditingController> _controllerList = [];
List<TextEditingController> _numControllerList = [];
List<Widget> _ingList = [];
#override
void initState() {
for (int i = 1; i <= widget.countIngs; i++) {
TextEditingController controller = TextEditingController();
TextField textField = TextField(
controller: controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Ingredient $i',
),
);
TextEditingController numcontroller = TextEditingController();
TextField numField = TextField(
controller: numcontroller,
decoration: InputDecoration(
border: OutlineInputBorder(), hintText: '#', labelText: '#'),
keyboardType: TextInputType.number,
);
_ingList.add(Row(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: SizedBox(
width: 250,
child: textField,
)),
Text('x', style: GoogleFonts.biryani(fontSize: 15)),
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: SizedBox(
width: 75,
child: numField,
))
],
));
_controllerList.add(controller);
_numControllerList.add(numcontroller);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return new Container(
child: Flexible(
child: ListView(children: _ingList),
),
);
}
}
The only problem is that if you press the plus button after you have already entered values into one of the textFields, it will clear the field. I kind of understand why this is happening, but is there a way to work around this?
I might be missing some proper disposal of textControllers but here's the gist. As for further reading into keys and why they're necessary, I'd read this medium post
class ParentWidget extends StatefulWidget {
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
final _controllerList = <TextEditingController>[];
final _numControllerList = <TextEditingController>[];
/*
If the user had previous ingredients (say from a db), then you
would fill _controllerList and _numControllerList here using
the old ingredients to populate.
#override
void initState() {
for (ingredient in previousIngredients) {
final controller = TextEditingController(text: ingredient.text);
final numController = TextEditingController();
_controllerList.add(controller);
_numControllerList.add(numController);
}
super.initState();
}
*/
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 30.0),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Text('Ingredients'),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
_controllerList.add(TextEditingController());
_numControllerList.add(TextEditingController());
});
},
),
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
if (_controllerList.isEmpty) return;
setState(() {
_controllerList.removeLast();
_numControllerList.removeLast();
});
},
)
],
),
SizedBox(height: 10.0),
ListOfIngsWidget(_controllerList, _numControllerList),
],
),
),
);
}
}
class ListOfIngsWidget extends StatelessWidget {
ListOfIngsWidget(this.controllerList, this.numControllerList)
: assert(controllerList.length == numControllerList.length);
final List<TextEditingController> controllerList;
final List<TextEditingController> numControllerList;
#override
Widget build(BuildContext context) {
final _ingList = <Widget>[];
for (var i = 0; i < controllerList.length; i++) {
final textField = TextField(
controller: controllerList[i],
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Ingredient $i',
),
);
final numField = TextField(
controller: numControllerList[i],
decoration: InputDecoration(
border: OutlineInputBorder(), hintText: '#', labelText: '#'),
keyboardType: TextInputType.number,
);
_ingList.add(
Row(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: SizedBox(
width: 250,
child: textField,
)),
Text('x', style: GoogleFonts.biryani(fontSize: 15)),
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: SizedBox(
width: 75,
child: numField,
),
)
],
),
);
}
return Container(
child: Flexible(
child: ListView(children: _ingList),
),
);
}
}