country_phone_field 0.1.1 copy "country_phone_field: ^0.1.1" to clipboard
country_phone_field: ^0.1.1 copied to clipboard

A highly customizable international phone number input field: searchable country picker, per-country validation, flexible styling, and E.164 output.

example/lib/main.dart

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

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

/// A small gallery app showing several configurations of [PhoneNumberField]:
/// the default outlined style, a borderless/filled style, an underline style,
/// a locked single-country field, a dialog picker, and a fully custom
/// decoration — plus live output and form validation.
class ExampleApp extends StatefulWidget {
  const ExampleApp({super.key});

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  ThemeMode _themeMode = ThemeMode.light;

  @override
  Widget build(BuildContext context) {
    final seed = const Color(0xFF6C5CE7);
    return MaterialApp(
      title: 'country_phone_field demo',
      debugShowCheckedModeBanner: false,
      themeMode: _themeMode,
      theme: ThemeData(colorSchemeSeed: seed, useMaterial3: true),
      darkTheme: ThemeData(
        colorSchemeSeed: seed,
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      home: GalleryPage(
        isDark: _themeMode == ThemeMode.dark,
        onToggleTheme: () => setState(() {
          _themeMode = _themeMode == ThemeMode.dark
              ? ThemeMode.light
              : ThemeMode.dark;
        }),
      ),
    );
  }
}

class GalleryPage extends StatefulWidget {
  const GalleryPage({
    required this.isDark,
    required this.onToggleTheme,
    super.key,
  });

  final bool isDark;
  final VoidCallback onToggleTheme;

  @override
  State<GalleryPage> createState() => _GalleryPageState();
}

class _GalleryPageState extends State<GalleryPage> {
  final _formKey = GlobalKey<FormState>();
  PhoneNumber? _live;
  String? _submitted;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('country_phone_field'),
        actions: [
          IconButton(
            tooltip: 'Toggle theme',
            icon: Icon(widget.isDark ? Icons.light_mode : Icons.dark_mode),
            onPressed: widget.onToggleTheme,
          ),
        ],
      ),
      body: Form(
        key: _formKey,
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            // Live output banner.
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Live output',
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 8),
                    Text('Country: ${_live?.country.name ?? '—'}'),
                    Text('National: ${_live?.nationalNumber ?? '—'}'),
                    Text('E.164: ${_live?.completeNumber ?? '—'}'),
                    Text('Valid: ${_live?.isValid ?? false}'),
                    if (_submitted != null) ...[
                      const Divider(),
                      Text('Submitted: $_submitted'),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(height: 24),

            // 1. Default outlined, validated, drives the live banner.
            _Section(
              title: '1 · Default (outlined + validation)',
              child: PhoneNumberField(
                initialCountry: Countries.zimbabwe,
                pickerConfig: const CountryPickerConfig(
                  favorites: ['ZW', 'ZA', 'KE', 'NG'],
                ),
                onChanged: (value) => setState(() => _live = value),
              ),
            ),

            // 2. Borderless / filled, rounded.
            _Section(
              title: '2 · Borderless filled',
              child: PhoneNumberField(
                initialCountry: Countries.kenya,
                borderType: PhoneFieldBorderType.none,
                borderRadius: 28,
                fillColor: Theme.of(
                  context,
                ).colorScheme.surfaceContainerHighest,
                labels: const PhoneFieldLabels(labelText: 'Mobile'),
              ),
            ),

            // 3. Underline style, no flag, ISO code instead of dial code.
            _Section(
              title: '3 · Underline · ISO code · no divider',
              child: const PhoneNumberField(
                initialCountry: Countries.unitedStates,
                borderType: PhoneFieldBorderType.underline,
                filled: false,
                selectorStyle: CountrySelectorStyle(
                  showFlag: false,
                  showIsoCode: true,
                  showDivider: false,
                ),
              ),
            ),

            // 4. Locked single country (picker disabled).
            _Section(
              title: '4 · Locked country (mobile-money style)',
              child: const PhoneNumberField(
                lockedCountry: Countries.zimbabwe,
                labels: PhoneFieldLabels(labelText: 'EcoCash number'),
              ),
            ),

            // 5. Dialog picker + custom dropdown icon.
            _Section(
              title: '5 · Dialog picker',
              child: const PhoneNumberField(
                initialCountry: Countries.india,
                pickerConfig: CountryPickerConfig(
                  type: CountryPickerType.dialog,
                ),
                selectorStyle: CountrySelectorStyle(
                  dropdownIcon: Icon(Icons.expand_more, size: 18),
                ),
              ),
            ),

            // 6. Fully custom InputDecoration.
            _Section(
              title: '6 · Fully custom decoration',
              child: PhoneNumberField(
                initialCountry: Countries.nigeria,
                decoration: InputDecoration(
                  labelText: 'WhatsApp',
                  prefixIconConstraints: const BoxConstraints(
                    minWidth: 0,
                    minHeight: 0,
                  ),
                  filled: true,
                  fillColor: Theme.of(
                    context,
                  ).colorScheme.primaryContainer.withValues(alpha: 0.3),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                    borderSide: BorderSide(
                      color: Theme.of(context).colorScheme.primary,
                      width: 1.5,
                    ),
                  ),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                    borderSide: BorderSide(
                      color: Theme.of(context).colorScheme.tertiary,
                      width: 2,
                    ),
                  ),
                ),
              ),
            ),

            const SizedBox(height: 8),
            FilledButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  setState(
                    () => _submitted = _live?.completeNumber ?? '(empty)',
                  );
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Valid! Submitted.')),
                  );
                }
              },
              child: const Text('Validate & submit'),
            ),
            const SizedBox(height: 32),
          ],
        ),
      ),
    );
  }
}

class _Section extends StatelessWidget {
  const _Section({required this.title, required this.child});

  final String title;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: Theme.of(context).textTheme.labelLarge),
          const SizedBox(height: 8),
          child,
        ],
      ),
    );
  }
}
0
likes
160
points
--
downloads

Documentation

API reference

Publisher

unverified uploader

A highly customizable international phone number input field: searchable country picker, per-country validation, flexible styling, and E.164 output.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on country_phone_field