Creating custom keypads for your Flutter apps.

Henry Ifebunandu
5 min readFeb 21, 2023

--

An image title.

Introduction

As a mobile developer, you might have encountered apps or UI for app designs that feature custom keyboards for PINs, OTP or some other use case and wondered how you would implement such. I for one came across a scenario where I had to create one and had some trouble, it wasn’t hard per se, but due to some other use case or logic I had to factor in, it proved pretty tedious to implement. We are going to be creating a Numeric keyboard but I believe the logic applied here can be used to create more advanced keyboards like Alphabetical or symbolical keyboards.

To proceed with this tutorial, some basic knowledge of Flutter is required.

Let’s get down to business

First off, like every classic Flutter tutorial we need to create a new Flutter project, when that’s done, go ahead and clear some of the default code that was generated. Your code should look similar to this:

Let’s create our simple numeric keyboard UI…

  • 1: We build a 3x4 keyboard layout consisting of our desired individual keys.
  • 2: Method to return a single key, this takes the text to input and an onPressed callback if you need it.
  • 3: Method to construct our text input.
  • 4: We would also need a backspace implementation.

For our _input method, we can do something as simple as:

void _input(String text) {
final value = controller.text + text;
controller.text = value;
}

This simply takes the text and appends it to whatever text we have in our controller.

For our _backspace method, we can do something like this:

void _backspace() {
final value = controller.text;
if (value.isNotEmpty) {
controller.text = value.substring(0, value.length - 1);
}
}

This erases the last character in our controller’s text.

Our Numeric keypad now looks like this…

We can now insert a Textfield and our NumericKeyboard into our Home widget like so:

We do not need our default keyboard to pop up when we tap on the Textfield, so we can set the keyboardType of our TextField to TextInputType.none:

          TextField(
controller: textController,
keyboardType: TextInputType.none,
),

So far…

What we have currently might be all you need for your use case, but while using it you might have noticed a few kinks.

While typing, the cursor is at the start of the text on the Textfield and not at the end, also, on clicking our backspace we can only erase the last text on our Textfield. That’s not all. Wherever we place the cursor, we expect that whatever text we input next falls at the point where the cursor is placed, we also expect the same for our backspace, wherever the cursor is placed, erasing should begin from there.

Notice how the issues are somehow linked to our cursor. So we need a way to control our cursor and also know its position at every point in time. To get the cursor position as it moves in the TextField we need to add a listener to our controller. We can easily get the position by using contoller.selection.base.offset. This is a way to get the index (position) of the cursor, adding a listener directly updates our selection whenever there is a change, thereby updating the position also.

Update to our NumericKeypad

We would like the cursor to move to the end as we type and whenever we change the cursor position, text inputs start at the point where our cursor is placed. So update the _input method like so:

We had to take into consideration two scenarios when the TextField is empty and when it's not.

When it’s not empty:

  • 1: suffix - the string from the position of the cursor to the end of the text in the controller, basically all text after the cursor.
  • 2: value.substring(0, position) gets all the text before the cursor, appends the new input to the text and appends the suffix to it.
  • 3: The controller text is updated to the value (updated text input).
  • 4: The cursor position is updated.

When it’s empty:

  • 5: Append controller text to new input and assign to value.
  • 6: The controller text is updated.
  • 7: Since this is the first input set the position of the cursor to 1, so the cursor is placed at the end

Now, we also like to erase text starting from the position where our pointer resides. To achieve this we have to make modifications to our _backspace function:

  • 1: For our backspace, we only need erasing to work when our Textfield is not empty and the cursor position is not equal to 0 (at the start).
  • 2: suffix = string after the cursor position.
  • 3: Get all strings before the cursor and remove the last char in the string and append our suffix to it.
  • 4: The cursor position is updated.

This all looks good. The keyboard cursor moves and behaves as expected.

One more thing we might want is the ability to hide and show our keyboard. To achieve this we would use a FocusNode on our TextField and also listen for changes.

In our Home widget, after making it a stateful widget, we make more modifications like so:

  • 1: Create an object of FocusNode and instantiate.
  • 2: Add a listener to our focus on initState.
  • 3: Remove the listener and dispose focus on dispose
  • 4: Create an _onFocusChange and call setState
  • 5: Pass our _focus to the TextField.
  • 6: Show and hide our keyboard depending on whether hasFocus is true or false.

We would also need to pass our FocusNode to our NumericKeypad constructor because we want to add a button to the keypad that hides the keyboard using _focusNode.unfocus() .

                NumericKeypad(
controller: textController,
focusNode: _focus,
)

Update our NumericKeypad with the following highlighted code:

So basically we’ve passed down our FocusNode to gain the ability to hide the keyboard using _focusNode.unfocus().

_hideKeyboard simply calls _focusNode.unfocus().

How to dispose of controllers?

You might be tempted to dispose of the TextEditingController in the dispose method of NumericKeypad, I'm afraid that's not right because whenever we hide our keyboard the dispose method is called and if our controller is disposed of, we can’t call our controller again or we can but it throws an error saying we are trying to use an already disposed of controller.

So dispose of the TextEditingController in the dispose method of your Home widget.

// dispose method of our Home widget.

@override
void dispose() {
super.dispose();
textController.dispose(); <---------
}

That’s all folks…

This simple code implementation would be all you need to create your custom keyboard, you can however make modifications if need be. An example of such modifications would be creating an alphabetical keyboard, adding caps functionality, or switching from a numeric keyboard to an alphabetic keyboard. This covers the basics you might need.

You’ll find the link to the repository below (Please Star the repo if this article helped you). Please don’t forget to leave a few claps, this is my first post on Medium so please be nice 🥹. Also, I would like suggestions on how I might improve my writing, leave suggestions in the comments.

Gracias.

--

--