--

Developing Custom Flutter Pin Entry TextField

Smarter apps are being developed daily with a shorter time to market, so developers rely heavily on plugins, libraries, packages to achieve some tasks that ranges from basic to advance.

So, in this article i will work you through the process of building your own customized PinEntryTextField, or if you choose not to re-invent the wheel, you can search for my plugin in https://pub.dartlang.org/flutter “pin_entry_text_field” or fork the repo from https://github.com/prestigegodson/pin-entry-text-field/tree/master/pin_entry_text_field.

What we want to achieve

  • The widget will be able to accept pin digit of any length.
  • The Widget will fill up the text fields as you enter the pin.
  • The widget will detect the entry of the last pin field, which will then call your provided callback passing the pin entered as String argument.

All customizable attributes for PinEntryTextField

Attributes for PinEntryTextField

Step 1

Create a stateful widget by extending the StatefulWidget class.

import 'package:flutter/material.dart';

class PinEntryTextField extends StatefulWidget{
}

Step 2

Initialize the fields, onSubmit, fieldWidth, fontSize, isTextObscure, and showFieldAsBox class properties.

import 'package:flutter/material.dart';

class PinEntryTextField extends StatefulWidget{

int fields;
var onSubmit;
double fieldWidth;
double fontSize;
bool isTextObscure;
bool showFieldAsBox;

PinEntryTextField({this.fields : 4, this.onSubmit, this.fieldWidth : 40.0,
this.fontSize : 20.0, this.isTextObscure: false, this.showFieldAsBox : false}) : assert(fields > 0);


@override
State createState() {
return PinEntryTextFieldState();
}
}

Step 3

Create your PinEntryTextFieldState class which extends State of type PinEntryTextField.

class PinEntryTextFieldState extends State<PinEntryTextField>{

List<String> _pin;
List<FocusNode> _focusNodes;
List<TextEditingController> _textControllers;


@override
void initState() {
super.initState();
_pin = List<String>(widget.fields);
_focusNodes = List<FocusNode>(widget.fields);
_textControllers = List<TextEditingController>(widget.fields);
}


@override
void dispose() {
_focusNodes.forEach( (FocusNode f) => f.dispose());
_textControllers.forEach((TextEditingController t) => t.dispose());
}

void clearTextFields(){

_textControllers.forEach((TextEditingController tEditController) => tEditController.clear());
}

Widget buildTextField(int i, BuildContext context){

_focusNodes[i] = FocusNode();
_textControllers[i] = TextEditingController();

_focusNodes[i].addListener((){
if(_focusNodes[i].hasFocus) {
_textControllers[i].clear();
}
});

return Container(
width: widget.fieldWidth,
margin: EdgeInsets.only(right: 10.0),
child: TextField(
controller: _textControllers[i],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black, fontSize: widget.fontSize),
focusNode: _focusNodes[i],
obscureText: widget.isTextObscure,
decoration: InputDecoration(
counterText: "",
border: widget.showFieldAsBox ? OutlineInputBorder(borderSide: BorderSide(width: 2.0)) : null
),
onChanged: (String str){
_pin[i] = str;
if(i+1 != widget.fields){
FocusScope.of(context).requestFocus(_focusNodes[i+1]);
}else{
clearTextFields();
FocusScope.of(context).requestFocus(_focusNodes[0]);
widget.onSubmit(_pin.join());
}
},
onSubmitted: (String str){
clearTextFields();
widget.onSubmit(_pin.join());
},
),
);
}

Widget generateTextFields(BuildContext context){

List<Widget> textFields = List.generate(widget.fields, (int i){
return buildTextField(i, context);
});

FocusScope.of(context).requestFocus(_focusNodes[0]);

return Row(
mainAxisAlignment: MainAxisAlignment.center,
verticalDirection: VerticalDirection.down,
children: textFields
);
}

@override
Widget build(BuildContext context) {

return Container(
child: generateTextFields(context),
);
}


}

Lets break down the code a bit

The state class has three private class variables of type List:

  • _pin : This variable stores all the entered pin digits.
  • _textControllers : This variable stores all TextEditingController which is attached to each TextField, we can loop through the list of textEditControllers to clear the TextFields after the last pin has been entered.
void clearTextFields(){

_textControllers.forEach((TextEditingController tEditController) => tEditController.clear());
}
  • _focusNodes: This variable is used to store the FocusNode of each TextField widget, we use this variable to move the focus to the next TextField widget whenever a TextField value changes using the onChange TextField callback attribute.
onChanged: (String str){
_pin[i] = str;
if(i+1 != widget.fields){
FocusScope.of(context).requestFocus(_focusNodes[i+1]);
}else{
clearTextFields();
FocusScope.of(context).requestFocus(_focusNodes[0]);
widget.onSubmit(_pin.join());
}
}

Styling the Widget, we make use of these instance variables:

fieldWidth, fontSize, isTextObscure, showFieldAsBox

  • fieldWidth: The fieldWidth is set as the width of the Container widget wrapping each of the pin text field.
  • fontSize: The fontSize is used to style the font size of the TextField widget.
  • isTextObscure: The isTextObscure is a boolean variable used to set the obscure property of the TextField.
  • showFieldAsBox: Determines if the TextField should have an OutlineInputBorder at the 4 borders, or just a bottom underline.

With this i believe we are done here, you can customize this widget to suit your need. Thank you for reading

You can fork the repo from https://github.com/prestigegodson/pin-entry-text-field/tree/master/pin_entry_text_field.

--

--

Godson Ositadinma
Developing Custom Flutter Pin Entry TextField

A results-driven, customer-focused, articulate and analytical Senior Software Engineer who can think “out of the box.”