Number input on Flutter TextFields the RIGHT way!
Tags: flutter, developer, learning, dart
Published: 2024-05-26 05:48:29

Eureka!

That was my expression a few days ago, when I found the perfect algorithm to XYZ. You should have seen the excitement on my face, I couldn't wait for the weekend to finally share the experience 😁.

Since Flutter is used to develop Iphone and Android applications (two different platforms -> with two different keypads), I'll explain my thoughts clearly with images from an Emulator (Android) and a Simulator (iOS).

Let's dive in ...

**NUMBER INPUT ON FLUTTER TEXTFIELD**

In Flutter, when we want to collect number inputs from users, we set the [_keyboardType_](https://api.flutter.dev/flutter/material/TextField/keyboardType.html) property of a [_TextField_](https://api.flutter.dev/flutter/material/TextField/TextField.html) widget to [_TextInputType.number_](https://api.flutter.dev/flutter/services/TextInputType/number-constant.html)

![using number display](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6uywrw82xkkowk3th8ms.png)

Once you do this, you're simply asking the device's keyboard to display it's number display.

![side by side](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zqph3ogjy55zx019xmb2.png)


From the two images above, we are able to collect what we need, right? and, it's largely sufficient, right?

Most Flutter developers will stop at doing this. However, this article aims to expose the RIGHT WAY!

While the above code is enough to collect a user's age, it's not entirely good user experience, because, on a Platform like Android, user's will be able to type hyphens, commas, blank spaces and period (This particular concern will not be an issue on iOS, however, we are coming to that). 

![Android bad](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mya3aao9s6us6xe7cbvw.png)

What we can do is; Use the _inputFormatters_ property of a _TextField_ object to filter for just numbers. This ensures we constrain our inputs to DIGITS only.

![digits only](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sjvc8iy4ja2vbbeqx6gg.png)


**DECIMAL INPUT ON FLUTTER TEXTFIELD**

While the above solutions does exactly what we want, it is important I point out that, this is recommended if you're collecting just WHOLE NUMBERS from the user, a typical example being an age. If we are collecting decimals, say, a person's weight, we need to go through another path. Because, our first constraint does not allow period(decimals).
![allow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmrtt0cgjh1wtaiewnq2.png)

![Android decimal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d60srpo3yqsj6ty9aud1.png)

While this seems like a very nice approach, we are faced with a challenge, users are able to type a period more than once 😱, and we don't want that, AT ALL!

We need to find a way to stop users from entering a period more than once, luckily, the [_inputFormatters_](https://api.flutter.dev/flutter/material/TextField/inputFormatters.html) takes a List of _TextInputFormatter_, we can create our own custom [_TextInputFormatter_](https://api.flutter.dev/flutter/services/TextInputFormatter-class.html).

```
class SinglePeriodEnforcer extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final newText = newValue.text;
    // Allow only one period
    if ('.'.allMatches(newText).length <= 1) {
      return newValue;
    }
    return oldValue;
  }
}
```

![Single period enforcer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iof8wygtjiz7ztzvqe5n.png)

Awesome, there's yet another issue, and that is with Iphones, our current [_keyboardType_](https://api.flutter.dev/flutter/material/TextField/keyboardType.html) doesn't show a period on Iphones, remember the purpose of a cross-platform framework like Flutter is to write once and deploy everywhere.

![Iphone number only](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1uk951m8cmpd0e0nwa3.png)

Our solution would be to change the keyboard type to [_TextInputType.numberWithOptions_](https://api.flutter.dev/flutter/services/TextInputType/TextInputType.numberWithOptions.html) and set the [_decimal_](https://api.flutter.dev/flutter/services/TextInputType/decimal.html) parameter to _true_. This will ensure the period key appears at the bottom left.

![Iphone decimal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mzjkhxbmq1dcejuirh2.png)

You can explore the [_signed_](https://api.flutter.dev/flutter/services/TextInputType/signed.html) property of the _TextInputType.numberWithOptions_.


**FORMAT LIKE A PRO 😎**

While the two solutions above are largely sufficient for the kind of data we intend to collect from the user, I want to take the [_TextInputFormatter_](https://api.flutter.dev/flutter/services/TextInputFormatter-class.html) up a notch; that is where the "Aha" moment came, the "XYZ" solution.

_The Challenge_

When we want to collect information, such as an amount from a user, solution 2 above will be very nice, since the user is allowed to type decimals, however, I want to seperate the digits entered with "commas" automatically, without the user needing to do anything. This approach, I believe is very intuitive, it helps users visualise whatever amount they are entering, since it's comma seperated.

_The Solution_

For this solution, we need the [_intl_](https://pub.dev/packages/intl) package. Just include it in your pubspec.yaml file.

![flutter intl package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt7bzd6lf1mnzqlrqomx.png)

```
import 'package:intl/intl.dart';
import 'package:flutter/services.dart';

class ThousandsSeparatorInputFormatter extends TextInputFormatter {
  // Setup a formatter that supports both commas for thousands and decimals
  final formatter = NumberFormat("#,##0.###");

  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    if (newValue.text.isEmpty) {
      return newValue;
    }
    // Remove commas to check the new input and for parsing
    final newText = newValue.text.replaceAll(',', '');
    // Try parsing the input as a double
    final num? newTextAsNum = num.tryParse(newText);

    if (newTextAsNum == null) {
      return oldValue; // Return old value if new value is not a number
    }

    // Split the input into whole number and decimal parts
    final parts = newText.split('.');
    if (parts.length > 1) {
      // If there's a decimal part, format accordingly
      final integerPart = int.tryParse(parts[0]) ?? 0;
      final decimalPart = parts[1];
      // Handle edge case where decimal part is present but empty (user just typed the dot)
      final formattedText = '${formatter.format(integerPart)}.$decimalPart';
      return TextEditingValue(
        text: formattedText,
        selection: updateCursorPosition(formattedText),
      );
    } else {
      // No decimal part, format the whole number
      final newFormattedText = formatter.format(newTextAsNum);
      return TextEditingValue(
        text: newFormattedText,
        selection: updateCursorPosition(newFormattedText),
      );
    }
  }

  TextSelection updateCursorPosition(String text) {
    return TextSelection.collapsed(offset: text.length);
  }
}
```

Then, we use it, like so:

![Thousand seperator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ibk7eztqntc1lbd3foo.png)

We end up with something like this, a nicely formatted text input filter for collecting information, such as an amount

![both done](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u4oaai9zp55nqzvegtgf.png)

However 😩, we have one more tiny thing to fix, the value collected from the field has a "comma", we need to do away with that (We cannot be sending values with commas to the backend), you can create a function to strip the comma off, or you create an extension. I prefer the later though (Just a personal preference).

So, let's go ahead and create an extension on [_String_](https://api.flutter.dev/flutter/semantics/AttributedString/string.html)

![Comma formatter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/83mie5nuyortoop8df7o.png)

You just need to use it whereever you are collecting the value from the Field.

![usage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxqggt0af32drab9gl0z.png)

Here, I used the extension on the [_onChanged_](https://api.flutter.dev/flutter/material/TextField/onChanged.html) parameter of a [_TextField_](https://api.flutter.dev/flutter/material/TextField-class.html).

It's also important to note that; all the solutions we did works just as well on a [_TextFormField_](https://api.flutter.dev/flutter/material/TextFormField-class.html).

If you have any questions related to this article, you can leave them in the comment section and I'll attend to them as soon as I can 😊.

**HAPPY CODING!** ✌️

Feel free to connect with me:

- [Twitter](https://twitter.com/iGabbygreat)
- [WhatsApp](https://wa.me/+2348034339010)
- [LinkedIn](https://www.linkedin.com/in/gabbygreat)