Reputation: 10720
I recently wrote a test program that I needed that is essentially a CRUD program. I needed to handle this differently to other similar programs that I have written, because I normally use a stateful FAB widget, and don't have to setState() to enable and disable the FAB. In this test program I didn't want to use the custom FAB, and used the standard FAB. I found that whenever I had to enable or disable the FAB because of a change to a TextField, that this required a setState(), and after the build, the cursor for the TextField that was being edited had repositioned. I don't know why that happens, because I had not recreated the Widgets. The only solution that I could come up with to handle that issue was fairly messy and required saving the Widget position in the List of TextField and also save the Selection, and then after the build resetting the Selection to the saved Selection.
What I need to achieve is for the FAB to be only enabled when data has changed. Obviously this can vary with every key entry.
I presume I'm not handling this in the optimal way. How is this handled so that the cursor position remains as it was prior to the build?
----- Have Now Added Code below ----
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MyApp());
//=====================================================================================
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test Widgets',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(title: 'Test Widgets'),
);
}
}
//=====================================================================================
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_HomePageState createState() => _HomePageState();
}
//=====================================================================================
class _HomePageState extends State<HomePage> {
bool _tfDataHasChanged = false;
bool _tfInitialized = false;
bool _tfSaveSelection = false;
int _iNdxWidgetChanged = -1;
List<String> _lsOldData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<String> _lsNewData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<TextField> _lwTextFields;
TextSelection _wTextSelection;
//-------------------------------------------------------------------------------------
@override
void dispose() {
for (int iNdxWidget = 0;
_lwTextFields != null && iNdxWidget < _lwTextFields.length;
iNdxWidget++) {
_lwTextFields[iNdxWidget].focusNode.removeListener(() {
_fnFocusChanged();
});
_lwTextFields[iNdxWidget]?.controller?.dispose();
_lwTextFields[iNdxWidget]?.focusNode?.dispose();
}
super.dispose();
}
//-------------------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
_tfInitialized = false;
SchedulerBinding.instance.addPostFrameCallback((_) => _fnOnBuildComplete());
if (_lwTextFields == null) {
_fnCreateAllWidgets();
}
List<Widget> lwDisplay = _fnCreateDisplay();
return Scaffold(
appBar: AppBar(
flexibleSpace: SafeArea(
child: _fnCreateAppBarWidgets(),
)),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: lwDisplay,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _tfDataHasChanged ? _fnUpdateData : null,
tooltip: 'Update',
backgroundColor: _tfDataHasChanged ? Colors.blue : Colors.grey,
child: Icon(Icons.done),
),
);
}
//-------------------------------------------------------------------------------------
_fnOnBuildComplete() {
_tfInitialized = true;
if (_tfSaveSelection && _iNdxWidgetChanged >= 0) {
_lwTextFields[_iNdxWidgetChanged].controller.selection = _wTextSelection;
}
}
//-------------------------------------------------------------------------------------
void _fnCreateAllWidgets() {
_lwTextFields = List(_lsNewData.length);
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_fnCreateTextField(iNdxWidget);
}
}
//-------------------------------------------------------------------------------------
void _fnCreateTextField(int iNdxWidget) {
TextEditingController wController = TextEditingController();
FocusNode wFocusNode = FocusNode();
wFocusNode.addListener(() => _fnFocusChanged());
_lwTextFields[iNdxWidget] = TextField(
autofocus: false, //(iNdxWidget == 0),
autocorrect: false,
enabled: true,
keyboardType: TextInputType.text,
maxLength: 25,
controller: wController,
focusNode: wFocusNode,
textInputAction: TextInputAction.next /* TYPE OF ACTION KEY */,
onSubmitted: ((v) => _fnSetNextFocus(iNdxWidget)),
onChanged: (text) => _fnTextListener(iNdxWidget, text),
decoration: _fnCreateInputDecoration(
'Text Field Number ${iNdxWidget + 1}', 'Enter Data'),
style: _fnCreateWidgetTextStyle(Colors.blue[700]),
);
}
//-------------------------------------------------------------------------------------
_fnTextListener(int iNdxWidget, String sText) {
if (_tfInitialized) {
_lsNewData[iNdxWidget] = sText;
_fnCheckIfDataHasChanged(
iNdxWidget) /* ENABLE OR DISABLE SUBMIT BUTTON */;
}
}
//-------------------------------------------------------------------------------------
_fnSetNextFocus(int iNdxWidget) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_lwTextFields[iNdxWidget].focusNode.unfocus();
if (iNdxWidget + 1 < _lwTextFields.length) {
_lwTextFields[iNdxWidget + 1]?.focusNode?.requestFocus();
}
}
}
//-------------------------------------------------------------------------------------
InputDecoration _fnCreateInputDecoration(String sHeading, String sHint) {
return InputDecoration(
labelText: sHeading,
hintText: sHint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(20.0)),
);
}
//-------------------------------------------------------------------------------------
TextStyle _fnCreateWidgetTextStyle(Color color) {
return TextStyle(
fontSize: 14.0,
color: color,
);
}
//-------------------------------------------------------------------------------------
List<Widget> _fnCreateDisplay() {
List<Widget> lwDisplay = List((_lwTextFields.length * 2) + 2);
lwDisplay[0] = SizedBox(height: 10);
int iNdxDisplay = 1;
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
lwDisplay[iNdxDisplay++] = _lwTextFields[iNdxWidget];
lwDisplay[iNdxDisplay++] =
SizedBox(height: iNdxDisplay < lwDisplay.length - 2 ? 10 : 80);
}
lwDisplay[lwDisplay.length - 1] = Divider(color: Colors.black, height: 2);
return lwDisplay;
}
//-------------------------------------------------------------------------------------
_fnUpdateData() {
for (int iNdxWidget = 0; iNdxWidget < _lsNewData.length; iNdxWidget++) {
if (_lsNewData[iNdxWidget] != _lsOldData[iNdxWidget]) {
_lsOldData[iNdxWidget] = _lsNewData[iNdxWidget];
}
}
_fnCheckIfDataHasChanged(-1);
}
//-------------------------------------------------------------------------------------
_fnCheckIfDataHasChanged(int iNdxWidgetChanged) {
bool tfChanged = false /* INIT */;
for (int iNdxWidgetTest = 0;
!tfChanged && iNdxWidgetTest < _lsNewData.length;
iNdxWidgetTest++) {
tfChanged = _lsNewData[iNdxWidgetTest] != _lsOldData[iNdxWidgetTest];
}
if (iNdxWidgetChanged >= 0) {
_iNdxWidgetChanged = iNdxWidgetChanged;
_wTextSelection = _lwTextFields[iNdxWidgetChanged].controller.selection;
}
if (tfChanged != _tfDataHasChanged) {
setState(() => _tfDataHasChanged = tfChanged) /* WE NEED TO ENABLE FAB */;
}
}
//-------------------------------------------------------------------------------------
Row _fnCreateAppBarWidgets() {
IconData wIconData =
_tfSaveSelection ? Icons.check_box : Icons.check_box_outline_blank;
Color wColor = _tfSaveSelection ? Colors.blue[900] : Colors.grey[600];
IconButton wIconButton = IconButton(
icon: Icon(wIconData),
color: wColor,
onPressed: _fnCheckboxChanged,
iconSize: 40);
return Row(children: <Widget>[
SizedBox(width: 10),
Text('Save\nSelection', textAlign: TextAlign.center),
wIconButton,
SizedBox(width: 30),
Text('Test TextField')
]);
}
//-------------------------------------------------------------------------------------
_fnFocusChanged() {
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_iNdxWidgetChanged = iNdxWidget;
_wTextSelection = _lwTextFields[iNdxWidget].controller.selection;
return;
}
}
}
//-------------------------------------------------------------------------------------
void _fnCheckboxChanged() {
_tfSaveSelection = !_tfSaveSelection;
if (!_tfSaveSelection) {
_iNdxWidgetChanged = -1;
}
setState(() {});
}
}
-------- Have added key to TextField, but issue persists ---------
key: ValueKey<int>(iNdxWidget),
Upvotes: 2
Views: 3036
Reputation: 10720
My bug - as posted by @pskink
My excuse - I normally use a stateful FAB, so I don't normally encounter this.
Answer: so change this line:
TextEditingController wController = TextEditingController(text: _lsNewData[iNdxWidget]);
and remove this one
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
– pskink Feb 23 at 7:33
Upvotes: 1
Reputation: 537
I hope these functions might help you
void updateText(String text) {
if (text != null) {
this.text = _applyMask(mask, text);
} else {
this.text = '';
}
_lastUpdatedText = this.text;
}
void updateMask(String mask, {bool moveCursorToEnd = true}) {
this.mask = mask;
updateText(text);
if (moveCursorToEnd) {
this.moveCursorToEnd();
}
}
void moveCursorToEnd() {
final String text = _lastUpdatedText;
selection =
TextSelection.fromPosition(TextPosition(offset: (text ?? '').length));
}
Upvotes: 0