I am working on a custom-made OTP field widget in Flutter and encountering two issues:
Range Error: I am facing a range error when updating the parent controller with the text entered in the OTP fields.
Backspace Handling: When pressing the backspace button, the previous field should be cleared correctly, but it doesn’t work as expected.
Here is the relevant code for my custom OTP field widget:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomOtpField extends StatefulWidget {
final TextEditingController controller;
final int length;
final double size;
final double borderRadius;
final double borderWidth;
final Color focusedBorderColor;
final Color unfocusedBorderColor;
final Color backgroundColor;
final TextStyle textStyle;
final double fieldSpacing;
const CustomOtpField({
required this.controller,
this.length = 6,
this.size = 50.0,
this.borderRadius = 4.0,
this.borderWidth = 1.0,
this.focusedBorderColor = Colors.teal,
this.unfocusedBorderColor = Colors.grey,
this.backgroundColor = Colors.white,
this.textStyle = const TextStyle(fontSize: 20.0, color: Colors.black),
this.fieldSpacing = 12.0,
super.key,
});
@override
CustomOtpFieldState createState() => CustomOtpFieldState();
}
class CustomOtpFieldState extends State<CustomOtpField> {
late List<FocusNode> _focusNodes;
late List<TextEditingController> _fieldControllers;
@override
void initState() {
super.initState();
_focusNodes = List.generate(widget.length, (index) => FocusNode());
_fieldControllers =
List.generate(widget.length, (index) => TextEditingController());
for (int i = 0; i < widget.length; i++) {
_fieldControllers[i].addListener(() => _updateParentController(i));
}
widget.controller.addListener(_updateOtp);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_focusNodes.isNotEmpty) {
_focusNodes[0].requestFocus();
}
});
}
@override
void dispose() {
for (var node in _focusNodes) {
node.dispose();
}
for (var controller in _fieldControllers) {
controller.dispose();
}
widget.controller.removeListener(_updateOtp);
super.dispose();
}
void _updateParentController(int index) {
final text = _fieldControllers[index].text;
final currentText = widget.controller.text;
if (index >= 0 && index < widget.length) {
String newText;
if (text.isNotEmpty) {
newText = (index > 0 ? currentText.substring(0, index) : '') +
text +
(index < widget.length - 1 ? currentText.substring(index + 1) : '');
} else {
newText = (index > 0 ? currentText.substring(0, index) : '') +
(index < widget.length - 1 ? currentText.substring(index + 1) : '');
}
widget.controller.text = newText;
widget.controller.selection = TextSelection.fromPosition(
TextPosition(offset: newText.length),
);
}
}
void _updateOtp() {
final text = widget.controller.text;
if (text.length > widget.length) {
// Ensure the OTP length does not exceed the specified length
widget.controller.text = text.substring(0, widget.length);
}
if (text.length == widget.length) {
_focusNodes.last.unfocus();
} else if (text.length < widget.length) {
_focusNodes[text.length].requestFocus();
}
for (int i = 0; i < widget.length; i++) {
if (i < text.length) {
_fieldControllers[i].text = text[i];
} else {
_fieldControllers[i].clear();
}
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.length, (index) {
return Row(
children: [
_buildOtpField(index),
if (index < widget.length - 1) SizedBox(width: widget.fieldSpacing),
],
);
}),
);
}
Widget _buildOtpField(int index) {
return SizedBox(
height: widget.size,
width: widget.size,
child: TextField(
focusNode: _focusNodes[index],
controller: _fieldControllers[index],
onChanged: (value) {
if (value.length <= 1) {
if (value.isNotEmpty) {
if (index < widget.length - 1) {
_focusNodes[index + 1].requestFocus();
}
} else {
if (index > 0) {
_focusNodes[index - 1].requestFocus();
}
}
_updateParentController(index);
}
},
textAlign: TextAlign.center,
maxLength: 1,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
counterText: '',
contentPadding: EdgeInsets.zero,
filled: true,
fillColor: widget.backgroundColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
borderSide: BorderSide(
color: _focusNodes[index].hasFocus
? widget.focusedBorderColor
: widget.unfocusedBorderColor,
width: widget.borderWidth,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
borderSide: BorderSide(
color: widget.focusedBorderColor,
width: widget.borderWidth,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
borderSide: BorderSide(
color: widget.unfocusedBorderColor,
width: widget.borderWidth,
),
),
),
style: widget.textStyle,
),
);
}
}
Issues:
Range Error: I encounter a range error when updating the parent controller with the text entered in the OTP fields. How can I handle this without causing a range error?
Backspace Handling: When pressing the backspace button, the previous field should be cleared correctly, but it doesn’t work as expected. How can I fix this issue?
Additional Context:
I want to make sure that backspace correctly clears the current field and moves the focus to the previous field if needed.
The TextEditingController for each OTP field is managed within the widget.
Thank you for your help!