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 TextInputFormatter
s 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:
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:
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: