Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 156 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,124 +3,215 @@
[![Pub Version](https://img.shields.io/pub/v/number_editing_controller)](https://pub.dev/packages/number_editing_controller)
[![License](https://img.shields.io/github/license/nerdy-pro/flutter_number_editing_controller)](https://github.com/nerdy-pro/flutter_number_editing_controller/blob/main/LICENSE)

A Flutter `TextEditingController` that automatically formats numbers, decimals, and currencies as the user types. Built with full locale support.
A Flutter `TextEditingController` that formats numbers, decimals, and currencies as the user types. Supports locale-aware grouping, decimal separators, currency symbols, and live option changes without recreating the controller.

Developed by [nerdy.pro](https://nerdy.pro).

## Features
## What does it do?

- **As-you-type formatting** for integers, decimals, and currency amounts
- **Locale-aware** grouping and decimal separators (e.g. `1,234.56` in English, `1.234,56` in German)
- **Currency support** with automatic symbol placement based on locale
- **Extracts the numeric value** via `controller.number`
- **Negative number support** with optional `allowNegative` flag
- **Custom separators** for decimal and grouping characters
Drop a `NumberEditingTextController` into any `TextField`. The user sees formatted text (`$1,234.56`), and your code reads the raw numeric value via `controller.number`.

![number_editing_controller demo](https://raw.githubusercontent.com/nerdy-pro/flutter_number_editing_controller/main/img/screenshot.gif)
- Formats integers, decimals, and currencies as the user types
- Places grouping separators (`1,000,000`) and decimal separators (`3.14`) based on locale
- Positions currency symbols before or after the number depending on locale rules
- Extracts the underlying `num?` value at any time
- Supports negative numbers with an optional `allowNegative` flag
- Allows custom separators for grouping and decimal characters
- Lets you change locale, currency, separators, and precision at runtime

## Getting started
![number_editing_controller demo](https://raw.githubusercontent.com/nerdy-pro/flutter_number_editing_controller/main/img/screenshot.gif)

Install the package:
## Installation

```shell
flutter pub add number_editing_controller
```

## Usage
Requires Flutter 3.19+ and Dart 3.3+.

Create a controller and assign it to a `TextField`:
## Quick start

```dart
final controller = NumberEditingTextController.currency(currencyName: 'USD');
import 'package:number_editing_controller/number_editing_controller.dart';

// Create a currency controller
final controller = NumberEditingTextController.currency(
currencyName: 'USD',
locale: 'en',
);

// Use it in a TextField
TextField(
controller: controller,
keyboardType: TextInputType.numberWithOptions(decimal: true, signed: true),
)

// Read the value
final amount = controller.number; // e.g. 1234.56
```

Read the numeric value at any time:
## Controller types

```dart
final value = controller.number; // e.g. 1234.56
```
### Integer

### Integer input
Formats whole numbers with grouping separators.

```dart
final controller = NumberEditingTextController.integer();
final controller = NumberEditingTextController.integer(locale: 'en');
// User types "1000000" -> displays "1,000,000"
```

### Decimal input
### Decimal

Formats numbers with a decimal part. Control minimum and maximum fraction digits.

```dart
final controller = NumberEditingTextController.decimal();
final controller = NumberEditingTextController.decimal(
locale: 'en',
minimalFractionDigits: 2,
maximumFractionDigits: 4,
);
// User types "3.5" -> displays "3.50"
```

### Currency input
### Currency

Formats monetary amounts with a currency symbol placed according to locale rules.

```dart
final controller = NumberEditingTextController.currency(currencyName: 'EUR');
final controller = NumberEditingTextController.currency(
currencyName: 'EUR',
locale: 'de',
);
// controller.number = 1234.56 -> displays "1.234,56 €"
```

### Configuration options
## Configuration

### Shared parameters

All controller types accept these parameters, both in the constructor and as mutable properties:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `locale` | `String?` | Current locale | Locale for formatting (e.g. `'en'`, `'de'`, `'ja'`). |
| `groupSeparator` | `String?` | From locale | Symbol used to group digits (e.g. `,` in `1,000`). |
| `allowNegative` | `bool` | `true` | Whether to allow negative numbers. |
| `value` | `num?` | `null` | Initial numeric value (constructor only). |

All constructors accept the following parameters:
### Currency parameters

| Parameter | Description |
|-----------|-------------|
| `locale` | Locale for number formatting (e.g. `'en'`, `'de'`, `'ja'`). Defaults to the current locale. |
| `allowNegative` | Whether to allow negative numbers. Defaults to `true`. |
| `groupSeparator` | Custom digit grouping symbol (e.g. `','`, `'.'`, `' '`). |
| `value` | Initial numeric value. |
Available on `CurrencyEditingController` and `NumberEditingTextController.currency()`:

The `currency()` constructor also accepts:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `currencyName` | `String?` | From locale | ISO 4217 currency code (e.g. `'USD'`, `'EUR'`, `'JPY'`). |
| `currencySymbol` | `String?` | From currency code | Custom currency symbol (e.g. `'$'`, `'€'`, `'₺'`). |
| `decimalSeparator` | `String?` | From locale | Symbol used to separate the decimal part. |

| Parameter | Description |
|-----------|-------------|
| `currencyName` | ISO 4217 currency code (e.g. `'USD'`, `'EUR'`, `'JPY'`). |
| `currencySymbol` | Custom currency symbol (e.g. `'$'`, `'€'`, `'₺'`). |
| `decimalSeparator` | Custom decimal separator symbol. |
### Decimal parameters

The `decimal()` constructor also accepts:
Available on `DecimalEditingController` and `NumberEditingTextController.decimal()`:

| Parameter | Description |
|-----------|-------------|
| `minimalFractionDigits` | Minimum number of decimal digits to display. |
| `maximumFractionDigits` | Maximum number of decimal digits allowed. |
| `decimalSeparator` | Custom decimal separator symbol. |
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `minimalFractionDigits` | `int?` | From locale pattern | Minimum number of decimal digits to display. |
| `maximumFractionDigits` | `int?` | From locale pattern | Maximum number of decimal digits allowed. |
| `decimalSeparator` | `String?` | From locale | Symbol used to separate the decimal part. |

### Examples
## Changing options at runtime

All formatting options are mutable. Change them at any time and the displayed text updates automatically. This is useful for currency pickers, locale switchers, or toggling negative input.

```dart
// US Dollar with locale
final usd = NumberEditingTextController.currency(
final controller = CurrencyEditingController(
currencyName: 'USD',
locale: 'en',
);

// Euro in German locale
final eur = NumberEditingTextController.currency(
currencyName: 'EUR',
locale: 'de',
);
// User switches currency
controller.currencyName = 'EUR'; // "$1,234.56" -> "€1,234.56"

// User switches locale
controller.locale = 'de'; // "€1,234.56" -> "1.234,56 €"

// Disable negative input
controller.allowNegative = false; // "-500" -> "500"

// Positive-only integer
final positive = NumberEditingTextController.integer(
allowNegative: false,
// Change group separator
controller.groupSeparator = ' '; // "1.234" -> "1 234"
```

### Using subclasses directly

The factory constructors (`NumberEditingTextController.currency()`, `.decimal()`, `.integer()`) return the appropriate subclass. You can also instantiate them directly to access type-specific setters:

```dart
final controller = CurrencyEditingController(
currencyName: 'USD',
locale: 'en',
);
controller.currencyName = 'GBP'; // Only available on CurrencyEditingController

// Decimal with precision control
final precise = NumberEditingTextController.decimal(
minimalFractionDigits: 2,
maximumFractionDigits: 4,
final decimal = DecimalEditingController(
locale: 'en',
maximumFractionDigits: 4,
);
decimal.maximumFractionDigits = 2; // Only available on DecimalEditingController
```

A working example app is available in the [example](https://github.com/nerdy-pro/flutter_number_editing_controller/tree/main/example) directory.
## Class hierarchy

| Class | Description |
|-------|-------------|
| `NumberEditingTextController` | Interface. Use the factory constructors or program against this type. |
| `CurrencyEditingController` | Formats currency amounts. Mutable: `currencyName`, `currencySymbol`, `decimalSeparator`. |
| `DecimalEditingController` | Formats decimal numbers. Mutable: `minimalFractionDigits`, `maximumFractionDigits`, `decimalSeparator`. |
| `IntegerEditingController` | Formats integers. No type-specific options beyond the shared ones. |

## Locale examples

| Locale | Type | Value | Formatted |
|--------|------|-------|-----------|
| `en` | Currency (USD) | `1234.56` | `$1,234.56` |
| `de` | Currency (EUR) | `1234.56` | `1.234,56 €` |
| `fr` | Currency (EUR) | `1234.56` | `1 234,56 €` |
| `ja` | Currency (JPY) | `1500` | `¥1,500` |
| `ru` | Currency (RUB) | `500` | `500 ₽` |
| `en` | Integer | `1234567` | `1,234,567` |
| `de` | Integer | `1234567` | `1.234.567` |
| `en` | Decimal (max 2) | `1234567.89` | `1,234,567.89` |

## Known limitations

### Disposing the controller
### Number precision

The controller uses Dart's `num` type (`int` and `double`).

- **Flutter web**: JavaScript represents all numbers as 64-bit floats. Integers above `2^53 - 1` (9,007,199,254,740,991) lose precision silently. This affects both typed input and programmatic values.
- **Floating-point rounding**: `double` provides ~15-17 significant digits. Values like `12345678901234.56` will lose the fractional part. Standard IEEE 754 rounding artifacts apply (e.g. `1.005.toStringAsFixed(2)` produces `"1.00"`).
- **Native platforms**: 64-bit integers support up to `2^63 - 1` (~9.2 quintillion).

### Grouping

The library uses a uniform group size derived from the locale's ICU format pattern (typically groups of 3). **Variable-width grouping systems are not supported.** This affects the Indian/South Asian numbering system (lakh/crore), where groups alternate between 2 and 3 digits. For example, `12,34,56,789` would render as `123,456,789` instead.

### Decimal fraction digits

- Maximum of 20 fraction digits (Dart's `toStringAsFixed` limit). Values beyond 20 are clamped.
- `minimalFractionDigits` must not exceed `maximumFractionDigits`. No runtime validation is performed; invalid combinations will cause errors.

### Locale and formatting

- **No RTL support**: Arabic, Hebrew, and other right-to-left locales are not handled. Characters are inserted left-to-right.
- **Single-character decimal separator**: Multi-character decimal separators are not supported.
- **No input length limit**: Extremely long inputs may degrade formatting performance.

### Runtime option changes

Changing formatting options (locale, currency, separators) at runtime reformats from the stored numeric value. Any partial typing state (e.g. a trailing decimal separator the user just entered) is discarded.

## Disposing the controller

Like any `TextEditingController`, dispose of it when it is no longer needed:

Expand All @@ -131,3 +222,7 @@ void dispose() {
super.dispose();
}
```

## Example app

A working example app with currency picker, locale switcher, and negative toggle is available in the [example](https://github.com/nerdy-pro/flutter_number_editing_controller/tree/main/example) directory.
44 changes: 6 additions & 38 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,67 +10,35 @@ void main() {
class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'Number Editing Controller',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a blue toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
home: const HomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
class HomePage extends StatelessWidget {
const HomePage({super.key});

// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.

// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
title: const Text('Number Editing Controller'),
bottom: const TabBar(
tabs: [
Tab(text: 'Currency'),
Tab(text: 'Integer'),
Tab(text: 'Double'),
Tab(text: 'Decimal'),
],
),
),
Expand Down
Loading
Loading