I have a List of custom widgets that contain a TextFormField and use a TextEditingController which I initialize in the initState() function of the custom widget. I have wrapped the custom Widget in a Dismissible Widget to remove them from a list using the onDismissed function. This all looks like this:
#override
void initState() {
super.initState();
widget.nameController = TextEditingController(text: widget.initialName);
widget.amountController = TextEditingController(text: widget.initialAmount);
}
#override
void dispose() {
widget.nameController.dispose();
widget.amountController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
CreateOrEditRecipe createOrEditRecipe =
Provider.of<CreateOrEditRecipe>(context);
return Dismissible(
key: Key(widget.id),
background: buildSwipeActionRight(),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
createOrEditRecipe.removeIngredientCard(widget.id);
},
child: Container(
child: Row(
children: <Widget>[
SizedBox(
width: SizeConfig.blockSizeHorizontal * 60,
child: TextFormField(
controller: widget.nameController,
decoration: const InputDecoration(
labelText: "Foo",
labelStyle: TextStyle(fontSize: 12),
),
),
),
],
),
),
);
removeIngredientCard(String toRemove) {
_ingredientCards.removeWhere((element) => element.id == toRemove);
notifyListeners();
}
After removing the UI is updated using notifyListeners().
This removes the custom Widget from the list correctly and the UI updates correctly aswell, but if I now select the TextFormField of the last of the remaining widgets in the list, I get an error.
This error does not appear when I remove the last widget of the list and then try to select the TextFormField of the last widget of the remaining ones.
Any ideas where the problem might be?
Related
say I have a song lyric app and there is just one Scaffold with a Text widget that displays the entire lyric and the lyrics are written in the format
....
Chorus:
...
....
....
and I have a FAB, onClick of which I need the text to auto scroll to the text "Chorus:", this text is literally in every song, but when the verses are a about 4+, they usually go off screen, so, user usually has to manually scroll to the chorus again after each verse that's beyond the screen height, but I need this to be done automatically at the tap of a button
scroll up till the string "chorus" is in view, how would I do this in flutter
TEXT
const kTheAstronomers = '''1. Yeah, you woke up in London
At least that's what you said
I sat here scrollin'
And I saw you with your aunt
A demon on your left
An angel on your right
But you're hypocritical
Always political
Chorus:
Say you mean one thing
But you go ahead and lie
Oh, you lie-lie, lie-lie
And you say you're not the bad type
2. Oh, you posted on Twitter
Said you had other plans
But your mother, she called me
Said, "Come have lunch with the fam"
Guess you didn't tell her that
You should've called me back
I guess you changed your number or somethin\' '''
LYRIC SCREEN
#override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
child: Text(
kTheAstronomers,
style: const TextStyle(
fontSize: 30,
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
),
),
),
),
)
floatingActionButton: FAB(onPressed: autoScrollToChorus),
,
You can create a GlobalKey and use the currentContext to scroll to the Chorus part.
final _key = GlobalKey()
Inside the autoScrollToChorus method you can add:
final context = _key.currentContext!;
await Scrollable.ensureVisible(context)
I found a way.
I had to change the way I displayed the text, instead of using one text widget, I used a ListView builder to display two texts, but before that, in initState, when my page receives the text, I split the text into a list of two separate texts, one containing the first part and the other containing from the Chorus down, then I give this list to the listview builder (you could also just use a column and create two separate widgets and just pass the scroll key to the second text, knowing it's the second part of the text)
final GlobalKey _key = GlobalKey();
void _autoScrollToChorus() async {
BuildContext context = _key.currentContext!;
await Scrollable.ensureVisible(context);
}
late List<String> lyricList;
#override
initState() {
lyricList =
kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
itemCount: lyricList.length,
itemBuilder: (context, idx) {
return Text(
key: idx == 1 ? _key : null,
lyricList[idx],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 30,
),
);
}),
),
floatingActionButton: lyricList.length > 1 ? FloatingActionButton(
onPressed: _autoScrollToChorus,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Chorus"),
),
) : null,
);
}
Thanks to #Priyaank I knew to use the key and scroll to a particular widget
a more advanced solution that makes it possible to hide the button when the chorus is in view USING THE SCROLLABLE_POSITIONED_LIST PACKAGE
final GlobalKey _key = GlobalKey();
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemListener = ItemPositionsListener.create();
late List<String> lyricList;
bool chorusIsVisible = true;
void _autoScrollToChorus() {
// BuildContext context = _key.currentContext!;
// await Scrollable.ensureVisible(context);
_itemScrollController.scrollTo(
index: 1,
duration: const Duration(milliseconds: 500),
alignment: 0.5
);
}
#override
initState() {
lyricList =
kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
super.initState();
if(lyricList.length > 1) {
_itemListener.itemPositions.addListener(() {
chorusIsVisible = _itemListener.itemPositions.value.where((item) {
final isTopVisible = item.itemLeadingEdge >= 0;
return isTopVisible;
}
).map((item) => item.index).toList().contains(1);
setState(() {});
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ScrollablePositionedList.builder(
itemScrollController: _itemScrollController,
itemPositionsListener: _itemListener,
itemCount: lyricList.length,
itemBuilder: (context, idx) {
return Text(
lyricList[idx],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 30,
),
);
}),
),
floatingActionButton: lyricList.length > 1 && !chorusIsVisible ? FloatingActionButton(
onPressed: _autoScrollToChorus,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Chorus"),
),
) : null,
);
}
}
I want to see what I am typing, but in my code the TextField stays under the keyboard.
I´d be pretty reliefed if any of you guys know a solution to move the TextField above the keyboard!
#override
_object1State createState() => _object1State();
}
class _object1State extends State<object1> {
String insert = '';
void change_insert(new_text){
setState(() {
insert = new_text;
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text(insert),
TextField(
onSubmitted: change_insert,
decoration: InputDecoration(
labelText: 'click here ',
),
),
],
),
);
}
}
would suggest you to use flutter's new form widget inside that widget you can pass multiple textformfields widgets .
Once you Use these 2 widgets , Flutter will automatically take care of keyboard and input everttime you start typing.
https://www.github.com/mrinaljain/flutter_shoping_cart/tree/master/lib%2Fscreens%2Fedit_product_screen.dart
I have implemented an listener in the following way:
#override
void didChangeDependencies() {
final SlotDataProvider slotDevice = Provider.of<SlotDataProvider>(context);
spptask.streamdevice.listen((device) {
setState(() {
slotDevice._devices[0].name = device.name;
print("Device data received: ${device.name} ");
});
}, onError: (error) {
print("Error: $error.message");
});
super.didChangeDependencies();
}
I listen on a splitted controller and the print "Device data received:..." is called but the widget is not actualized. In the build method I do the following:
...
#override
Widget build(BuildContext context) {
final slotProvider = Provider.of<SlotDataProvider>(context);
final deviceProvider = Provider.of<DeviceDataProvider>(context);
Device slotDevice = slotProvider.getDevice(widget.slot);
Device device = deviceProvider.getDevice(widget.slot);
_dropdownMenuItems = buildDropdownMenuItems(deviceProvider.get());
return ListTile(
title: Row(
children: <Widget>[
SizedBox(
width: 140,
child: DropdownButton(
isExpanded: true,
disabledHint: Text(slotDevice.name),
hint: Text(slotDevice.name),
value: device,
items: _dropdownMenuItems,
onChanged: (value) {
device.setDevice(value);
slotDevice.setDevice(value);
}),
),
SizedBox(width: 10),
SizedBox(width: 60, child: Text('SLOT#${slotDevice.slot}')),
],
),
subtitle: Text(slotDevice.bdaddr, style: TextStyle(fontSize: 10.0)),
leading: SizedBox(
height: 40,
width: 35,
child: UsbBatteryImageAsset(slot: widget.slot),
),
trailing: Icon(Icons.keyboard_arrow_right),
);
}
}
What is missing in the above code. The SlotDataProvider is a fix list of "Device" with attributes such as name, id and so on.
#EDIT
The problem has to do with the combobox. If I change an other field, it works.
Usually for widgets to be rebuilt based on the data updated we use streambuilders
this will cause the widget to rebuild every time there is a change in the stream
it seams that your widget is being built once with the first listening of the data
have you tried wrapping the gridview in a stateful builder ?
I'm trying to make a news section in my app. In this page that's gonna display the news, i want to be able to click anywhere on the page and get the news that is next in my list. So far no problem with that, but i wanted it to have a nice animation so i tried implementing AnimatedSwitcher, but i can't figure out why there is no animation showing.
I tried changing the hierarchy of my code. Putting the gesture detector inside the animated switcher and the other way around. Letting the main container outside or inside of it too. I tried an animation builder that would scale it just in case it wasnt obvious enough but nothing. Tried changing the duration too but that wasn't it.
class ShowNews extends StatefulWidget {
#override
_ShowNewsState createState() => _ShowNewsState();
}
class _ShowNewsState extends State<ShowNews> {
List<News> _news = [
News(title: 'OYÉ OYÉ', desc: 'bla bla bla bla bla'),
News(title: 'another one', desc: 'plus de bout d\'histoire'),
News(title: 'boum', desc: 'attention à l\'accident'),
News(title: 'Lorem ipsum', desc: 'Lorem ipsum in doloris'),
];
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
if (_currentIndex < _news.length - 1) {
_currentIndex++;
} else {
_currentIndex = 0;
}
});
},
child: Container(
height: 160,
padding: EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
child: AnimatedSwitcher(
duration: Duration(seconds: 5),
child: ColumnArticle(_news, _currentIndex),
),
),
);
}
}
Everything is working fine but the animation.
Edit: I tried adding a key to make it different but still no animation.
class ColumnArticle extends StatelessWidget {
final List<News> _news;
final int _currentIndex;
ColumnArticle(this._news, this._currentIndex);
#override
Widget build(BuildContext context) {
return Column(
key: ValueKey<int>(_currentIndex),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
_news[_currentIndex].title,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
),
),
SizedBox(
height: 10.0,
),
Text(
_news[_currentIndex].desc,
style: TextStyle(
fontSize: 14.0,
),
),
],
);
}
}
That happens because the AnimatedSwitcher will add an animation anytime it is rebuilt with a different child reference. However, in your widget lifecycle, you are always using a ColumnArticle as a child, thus, not actually swapping any widget type, that's where the ValueKey comes in play.
You can use the index as the reference for the key, but make sure it actually changes, otherwise it won't work and you also need to pass it to your ColumnArticle base widget (super).
So, your ColumnArticle should look like this:
class ColumnArticle extends StatelessWidget {
final List<News> _news;
final int _currentIndex;
ColumnArticle(this._news, this._currentIndex) : super(key: ValueKey<int>(_currentIndex));
...
}
Passing the same type of widget with different attributes will not trigger an animation since they are the same widgets for the framework. It's also mentioned in the description.
If the "new" child is the same widget type and key as the "old" child,
but with different parameters, then AnimatedSwitcher will not do a
transition between them, since as far as the framework is concerned,
they are the same widget and the existing widget can be updated with
the new parameters. To force the transition to occur, set a Key on
each child widget that you wish to be considered unique (typically a
ValueKey on the widget data that distinguishes this child from the
others).
Here is the code from AnimatedSwitcher that checks whether to animate or not:
if (hasNewChild != hasOldChild ||
hasNewChild && !Widget.canUpdate(widget.child, _currentEntry.widgetChild)) {
// Child has changed, fade current entry out and add new entry.
_childNumber += 1;
_addEntryForNewChild(animate: true);
}
This is the static canUpdate method from the framework:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
To solve this you can set individual keys to your News widgets based on their distinct attributes (eg. text, count, value). ValueKey<T> is just for that.
Column(
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: Text(
'$_count',
// This key causes the AnimatedSwitcher to interpret this as a "new"
// child each time the count changes, so that it will begin its animation
// when the count changes.
key: ValueKey<int>(_count),
),
),
RaisedButton(
child: const Text('Increment'),
onPressed: () {
setState(() {
_count += 1;
});
},
),
])
I have a Statefull Widget that contains a textfield. I would like to know when the user clicked outside the textfield. In order to do that I am using a FocusNode.
Now the problem is my callback does not get called when a user clicks outside of the texfield. This is my code
class TestText extends State<SingleFieldEdit>
{
TextEditingController _qTextEditController = new TextEditingController();
FocusNode _focus;
TestText();
#override
void initState() {
super.initState();
_focus = new FocusNode();
_focus.addListener(_onFocusChange);
}
#override
void dispose() {
super.dispose();
_focus.dispose();
}
void _onFocusChange(){
print("Focus changed - Does not get called when clicked outside of textbox");
if(_focus.hasFocus==false){
setState(() {
_enableEdit=false;
_qTextEditController.text = _existingText;
});
}
}
Widget getEditableField()
{
Widget containerBorder = new Container(
margin: const EdgeInsets.all(0.0),
padding: const EdgeInsets.only(top:0.0,bottom:0.0),
decoration: new BoxDecoration(
border: new Border(top:BorderSide(width: 0.4),bottom: BorderSide(width: 0.4) ),
),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Flexible(child:TextField(
focusNode: _focus,
controller: _qTextEditController,
decoration: InputDecoration.collapsed(hintText: 'Your hint goes here',enabled: _enableEdit),//InputDecoration(hintText: 'Your hint goes here', /*labelText: "Your Label goes here",*/ enabled: _enableEdit),
)),
getIcon(),
],)
);
return getEditableField
}
#override
Widget build(BuildContext context)
{
return getEditableField();
}
}
Wrap your Scaffold with a GestureDetector and set the onTap to Focus on another node
You'd need to focus another FocusNode
or calling unfocus on the focused FocusNode (https://github.com/flutter/flutter/issues/19552#issuecomment-410909751)
Please follow and upvote https://github.com/flutter/flutter/issues/20227 for an easier solution (perhaps also https://github.com/flutter/flutter/issues/7247).