Flutter - Currency Format inTextField/TextFormField

This tutorial shows you how to add a TextField or TextFormField whose value is displayed in currency format.

If your application has a field for inputting a number that represents a nominal, providing a suitable input field can provide a better user experience. Ideally, the displayed keyboard should be for inputting numbers. In addition, the inputted value should be formatted with separators to make it easier to check whether the value is already correct. Below are the things you need to do if you want to have such a field in a Flutter application.

Set Keyboard Type to Number

Displaying the default keyboard is irrelevant if you know that the field is only for nominal values. It's preferable to display a numeric pad which contains numeric digits and related symbols (such as -, ,, .). For a TextField or TextFormField, it can be done by passing a keyboardType argument. The appropriate value for this case is TextInputType.number, which is a static constant that creates a TextInputType optimized for numeric information. The example for TextField can be seen below.

  TextField(
    keyboardType: TextInputType.number,
    // other arguments
  )

Below is the same thing for a TextFormField.

  TextFormField(
    keyboardType: TextInputType.number,
    // other arguments
  )

TextInputType also has a named constructor where you can set to allow signed and/or decimal values. Using TextInputType.number is the same as using TextInputType.numberWithOptions without passing any argument.

  const TextInputType.numberWithOptions({
    bool signed = false,
    bool decimal = false,
  })

Usage example:

  TextField(
    keyboardType: const TextInputType.numberWithOptions(
      signed: true,
      decimal: true,
    ),
    // other arguments
  )

However, on many devices, the keyboard looks the same even if you change the argument values. The - key may still be displayed even when signed is false. The same also applies for the decimal point key which may still be visible even when decimal is false. In addition, setting the keyboard type only changes the displayed keyboard, but does not validate whether the value contains only allowed characters. As a result, a user can paste invalid characters to the field. Therefore, we need to sanitize the values. The next section explains how to do it, along with how to format the values.

Filter and Format Input

To improve the readability of the nominal, it would be better if the input field can automatically add delimiters. Usually, a thousand delimiter is added every 3 digits counted from the last digit. By adding delimiters, it helps users to recheck whether the inputted value is already correct.

Unfortunately, formatting numbers in a currency format for a text field is not as easy as formatting for a non-editable text. If you only need to display a value in a specified currency, you can do it easily using NumberFormat. The reason is we need to handle the value changes every time the user edits the field which include validating the value, formatting it, and setting the cursor to the correct index.

Below are the requirements of how a good currency-formatted input field should be.

  • Remove invalid characters. It should only accept allowed characters which include numeric characters and a decimal point (typically . or , depending on the locale).
  • Automatically add thousand delimiters (typically , or . depending on the locale). Users are not allowed to add the thousand delimiters since it's the responsibility of the application to handle it.
  • Should remove the cursor to the correct position. If the user adds a character, it should move the cursor after the newly added character. If the user deletes a character, it should move the cursor at the index of the deleted character. If the typed character is invalid (or doesn't affect the input), the cursor position should remain unchanged.
  • If the requirement allows a decimal point, it should be able to only add one decimal point. The fractional digits (numbers after the decimal point) can only be added after the user types the decimal point. If necessary, should be able to limit the number of the fractional digits (the precision).

In Flutter, you can add multiple TextInputFormatters to a TextField or TextFormField. A TextInputFormatter is a way to provide validation and formatting for the text that's being edited. It's invoked every time the text changes, including when the user types or performs cut/paste.

There are several packages that provide the functionality for formatting text fields in currency format. Some of them include currency_text_input_formatter, intl, and flutter_masked_text (or flutter_masked_text2). However, most of them are buggy especially regarding the cursor behavior that may jump to the end after typing in the middle of the text. In addition, using NumberFormat is not a good idea. That's because using NumberFormat tends to format the value to a certain number of decimal places which makes it very difficult to handle if the user hasn't input the fractional digits.

With the problems stated above, creating a custom TextInputFormatter can be the best solution. First, create a class that extends TextInputFormatter. The created class has to implement formatEditUpdate method. It's a method to be called when the text value changes.

The formatEditUpdate method has two parameters, oldValue and newValue. They contain the information before and after the text edited respectively represented as a TextEditingValue object. The return type is also a TextEditingValue. Our goal is to return a TextEditingValue with the correct text and selection values. The returned text should be a sanitized value in currency format. The selection should return the correct cursor index based on the last added or deleted character position. We are going to create two formatters. The one is for currency numbers with a decimal point, while the other is for currency numbers without a decimal point.

Below is the formatter that allows a decimal point. It uses , as the thousand separator and . as the decimal separator with the number of fractional digits is limited to 2. First, it validates whether the newValue's text matches against a regex pattern. It can only contain digits and commas, followed by an optional decimal point, followed by zero or more digits. If the validation fails, we should keep the oldValue. If the validation is successful, replace the characters other than digits and decimal point. The thousand separators are removed as well and we will be added later in the next step (so it's not necessary to validate whether the current thousand separators are already in the correct positions./p>

Then, we need to format the number before the decimal point to be separated by the thousand separators. It's easier to add the thousand separators from back to forth. So, we need to find the index of the decimal point, or the length of the text if it doesn't have a decimal point to be used as the starting index. From the index , decrement the index by 3 and add a thousand separator until the index is less than or equal to 0.

If you want to limit the number of fractional digits, it's necessary to count the length of characters after the decimal point. If it exceeds the limit, just remove the exceeding characters. We need to keep track of the number of removed digits since it affects the cursor position.

After that, check if the new formatted value is equal to the text of the oldValue, we can just return the oldValue since nothing is changed. It can happen in some cases, for example if the user types the thousand separators which doesn't affect the displayed value.

Next, handle to compute at which index the cursor should be. That's because adding a thousand separator may affect the index of the cursor. For example, if the previous value has two thousand separators but the new value has three thousand separators, we should increment the cursor index based on the difference of separator numbers. If the number of fractional digits is limited, it's also necessary to decrement the cursor index by the number of removed fractional digits.

  class CurrencyInputFormatter extends TextInputFormatter {

    final validationRegex = RegExp(r'^[\d,]*\.?\d*$');
    final replaceRegex = RegExp(r'[^\d\.]+');
    static const fractionalDigits = 2;
    static const thousandSeparator = ',';
    static const decimalSeparator = '.';

    @override
    TextEditingValue formatEditUpdate(
      TextEditingValue oldValue,
      TextEditingValue newValue
    ) {
      if (!validationRegex.hasMatch(newValue.text)) {
        return oldValue;
      }

      final newValueNumber = newValue.text.replaceAll(replaceRegex, '');

      var formattedText = newValueNumber;

      /// Add thousand separators.
      var index = newValueNumber.contains(decimalSeparator)
          ? newValueNumber.indexOf(decimalSeparator)
          : newValueNumber.length;

      while (index > 0) {
        index -= 3;

        if (index > 0) {
          formattedText = formattedText.substring(0, index)
              + thousandSeparator
              + formattedText.substring(index, formattedText.length);
        }
      }

      /// Limit the number of decimal digits.
      final decimalIndex = formattedText.indexOf(decimalSeparator);
      var removedDecimalDigits = 0;

      if (decimalIndex != -1) {
        var decimalText = formattedText.substring(decimalIndex + 1);

        if (decimalText.isNotEmpty && decimalText.length > fractionalDigits) {
          removedDecimalDigits = decimalText.length - fractionalDigits;
          decimalText = decimalText.substring(0, fractionalDigits);
          formattedText = formattedText.substring(0, decimalIndex)
              + decimalSeparator
              + decimalText;
        }
      }

      /// Check whether the text is unmodified.
      if (oldValue.text == formattedText) {
        return oldValue;
      }

      /// Handle moving cursor.
      final initialNumberOfPrecedingSeparators = oldValue.text.characters
          .where((e) => e == thousandSeparator)
          .length;
      final newNumberOfPrecedingSeparators = formattedText.characters
          .where((e) => e == thousandSeparator)
          .length;
      final additionalOffset = newNumberOfPrecedingSeparators - initialNumberOfPrecedingSeparators;

      return newValue.copyWith(
        text: formattedText,
        selection: TextSelection.collapsed(offset: newValue.selection.baseOffset + additionalOffset - removedDecimalDigits),
      );
    }
  }

Then, use the formatter in the TextField or TextFormField.

  TextField(
    inputFormatters: [
      CurrencyInputFormatter(),
    ],
    // other arguments
  )

Output:

Flutter - Currency TextField/TextFormField (Double)

For currency-formatted fields without a decimal point, the code is similar. The differences are it uses different regex patterns and it doesn't need the step for limiting fractional digits.

  class IntegerCurrencyInputFormatter extends TextInputFormatter {

    final validationRegex = RegExp(r'^[\d,]*$');
    final replaceRegex = RegExp(r'[^\d]+');
    static const thousandSeparator = ',';

    @override
    TextEditingValue formatEditUpdate(
      TextEditingValue oldValue,
      TextEditingValue newValue
    ) {
      if (!validationRegex.hasMatch(newValue.text)) {
        return oldValue;
      }

      final newValueNumber = newValue.text.replaceAll(replaceRegex, '');

      var formattedText = newValueNumber;

      /// Add thousand separators.
      var index = newValueNumber.length;

      while (index > 0) {
        index -= 3;

        if (index > 0) {
          formattedText = formattedText.substring(0, index)
              + thousandSeparator
              + formattedText.substring(index, formattedText.length);
        }
      }

      /// Check whether the text is unmodified.
      if (oldValue.text == formattedText) {
        return oldValue;
      }

      /// Handle moving cursor.
      final initialNumberOfPrecedingSeparators = oldValue.text.characters
          .where((e) => e == thousandSeparator)
          .length;
      final newNumberOfPrecedingSeparators = formattedText.characters
          .where((e) => e == thousandSeparator)
          .length;
      final additionalOffset = newNumberOfPrecedingSeparators - initialNumberOfPrecedingSeparators;

      return newValue.copyWith(
        text: formattedText,
        selection: TextSelection.collapsed(offset: newValue.selection.baseOffset + additionalOffset),
      );
    }
  }

Output:

Flutter - Currency TextField/TextFormField (Integer)

Display Currency Symbol

To display the currency symbol, the easiest way is by adding a text as the prefix or suffix argument of the TextField or TextFormField. It's easier than including the currency symbol as the value of the field, since you have to handle it in the formatter.

  TextField(
    prefix: 'Rp ',
  )

Full Code

  import 'package:flutter/material.dart';
  import 'package:flutter/services.dart';

  void main() => runApp(const MyApp());

  class MyApp extends StatelessWidget {

    const MyApp({Key? key}) : super(key: key);

    @override
    Widget build(BuildContext context) {
      return const MaterialApp(
        title: 'Woolha.com Flutter Tutorial',
        home: Home(),
      );
    }
  }

  class Home extends StatefulWidget {
    const Home({super.key});

    @override
    State<StatefulWidget> createState() {
      return HomeState();
    }
  }

  class HomeState extends State<Home> {

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Woolha.com Flutter Tutorial'),
          backgroundColor: Colors.teal,
        ),
        body: Padding(
          padding: const EdgeInsets.all(10),
          child: Column(
            children: [
              TextField(
                decoration: const InputDecoration(
                  labelText: 'Amount',
                  hintText: 'Enter the amount',
                  labelStyle: TextStyle(color: Colors.teal, fontSize: 20),
                  prefixText: 'Rp ',
                ),
                keyboardType: TextInputType.number,
                inputFormatters: [
                  CurrencyInputFormatter(),
                  // IntegerCurrencyInputFormatter(),
                ],
              ),
            ],
          ),
        ),
      );
    }
  }

  // See the code above for CurrencyInputFormatter and IntegerCurrencyInputFormatter

Summary

In this tutorial, we have learned how to create a TextField or TextFormField in Flutter where the value is displayed in currency format. Besides changing the keyboard type, it's necessary to use a custom TextInputFormatter. This tutorial has examples of how to create a custom formatter for numbers with currency format, for values with and without decimal points. If you need to use a different thousand or decimal separator, just adjust the regex patterns and the constants. You may need another adjustment if the currency uses a different logic for putting the separator, such as Indian Rupee.

You can also read about: