intl_phone_selector

A highly customizable international phone number selector for Flutter applications. This package provides both simple and advanced phone input widgets with comprehensive validation, error handling, and production-ready features.

Pub Version License: MIT

✨ Features

🎯 Core Features

  • 🌐 International Support - 200+ countries with accurate dial codes and flags
  • 🎨 Highly Customizable - Complete UI control with custom widgets
  • βœ… Smart Validation - Country-specific validation with real-time feedback
  • πŸ“± Auto-formatting - Automatic number formatting based on country standards
  • πŸ” Smart Search - Country search with multiple criteria (name, code, dial code)
  • πŸš€ Performance Optimized - Debounced validation and efficient rendering
  • πŸ›‘οΈ Error Resilient - Comprehensive error handling and fallbacks

πŸ”§ Advanced Features

  • πŸ“Š Multiple Input Styles - BasicPhoneInput and AdvancedPhoneInput widgets
  • 🎯 Preferred Countries - Configurable popular/recent countries
  • πŸ”„ Real-time Updates - Live validation status and formatting preview
  • πŸ“ˆ Business Logic Ready - Production patterns and validation rules
  • 🎨 Theme Support - Full Material Design 3 integration
  • πŸ› οΈ Developer Friendly - Comprehensive examples and documentation

Screenshots

Main Page Basic Example Advanced Example Country Picker
Main Page Basic Example Advanced Example Country Picker Modal

Intermediate Example: Intermediate Example

πŸ“¦ Installation

Add this to your package's pubspec.yaml file:

dependencies:
  intl_phone_selector: ^1.0.1

Then run:

flutter pub get

πŸš€ Quick Start

1️⃣ Basic Implementation (For Beginners)

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

class PhoneInputExample extends StatefulWidget {
  @override
  _PhoneInputExampleState createState() => _PhoneInputExampleState();
}

class _PhoneInputExampleState extends State<PhoneInputExample> {
  late PhoneNumberController _controller;
  bool _isValid = false;

  @override
  void initState() {
    super.initState();
    _controller = PhoneNumberController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Phone Input')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            BasicPhoneInput(
              controller: _controller,
              onValidationChanged: (isValid) {
                setState(() {
                  _isValid = isValid;
                });
              },
              decoration: InputDecoration(
                hintText: "Enter phone number",
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 16),
            Text('Valid: $_isValid'),
            SizedBox(height: 8),
            Text('Number: ${_controller.completeNumber}'),
          ],
        ),
      ),
    );
  }
}

Output:

Basic Example

2️⃣ Advanced Implementation (For Experienced Developers)

Use the AdvancedPhoneInput widget for more features and customization options:

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

class AdvancedPhoneExample extends StatefulWidget {
  @override
  _AdvancedPhoneExampleState createState() => _AdvancedPhoneExampleState();
}

class _AdvancedPhoneExampleState extends State<AdvancedPhoneExample> {
  late PhoneNumberController _controller;
  String? _errorText;
  bool _isValid = false;

  @override
  void initState() {
    super.initState();
    _controller = PhoneNumberController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Advanced Phone Input')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            AdvancedPhoneInput(
              controller: _controller,
              showValidationIcon: true,
              showPopularCountries: true,
              enableSearch: true,
              borderRadius: 12,
              errorText: _errorText,
              hintText: 'Enter your phone number',
              onValidationChanged: (isValid) {
                setState(() {
                  _isValid = isValid;
                  _errorText = isValid ? null : 'Invalid phone number';
                });
              },
              onCountryChanged: (country) {
                print('Selected: ${country.name}');
              },
            ),
            
            SizedBox(height: 16),
            
            Text('Status: ${_isValid ? "Valid" : "Invalid"}'),
            Text('Number: ${_controller.completeNumber}'),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Output:

Advanced Example

3️⃣ Custom Implementation (Complete Control)

For complete UI control, you can build custom interfaces while using the core functionality:

Widget _buildCustomPhoneInput() {
  return Container(
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(12),
      color: Colors.grey.shade100,
      border: Border.all(color: Colors.grey.shade400),
    ),
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
    child: Row(
      children: [
        // Custom country selector
        InkWell(
          onTap: () async {
            final country = await showYourCustomCountryPicker(context);
            if (country != null) {
              _controller.setCountry(country);
            }
          },
          child: Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Row(
              children: [
                Text(_controller.selectedCountry.flagEmoji),
                SizedBox(width: 4),
                Text(_controller.selectedCountry.dialCode),
                Icon(Icons.arrow_drop_down, size: 16),
              ],
            ),
          ),
        ),

        SizedBox(width: 12),

        // Custom phone field
        Expanded(
          child: TextField(
            controller: _controller.numberController,
            decoration: InputDecoration(
              border: InputBorder.none,
              hintText: 'Phone number',
            ),
            keyboardType: TextInputType.phone,
            onChanged: (_) {
              _controller.formatPhoneNumber();
            },
          ),
        ),
      ],
    ),
  );
}

Output:

Intermediate Example

πŸ“š API Reference

PhoneNumberController

The core controller class that manages phone number input, country selection, and validation.

Properties

final controller = PhoneNumberController();

// Core properties
String completeNumber;           // Full number with country code
String formattedNumber;          // Formatted display number
Country selectedCountry;         // Currently selected country
TextEditingController numberController; // Access to text input
bool isEmpty;                    // Check if input is empty

// Methods
bool isValid();                  // Validate current number
void setCountry(Country country); // Change country
void setPhoneNumber(String number); // Set number programmatically
void clearNumber();              // Clear input
void formatPhoneNumber();        // Apply formatting

Usage Examples

// Initialize with specific country
final controller = PhoneNumberController(
  initialCountry: CountriesData.getCountryByCode('GB'),
);

// Listen to changes
controller.addListener(() {
  print('Phone changed: ${controller.completeNumber}');
  print('Is valid: ${controller.isValid()}');
});

// Clean up
@override
void dispose() {
  controller.dispose();
  super.dispose();
}

Country

A model class that represents country data with dial code and flag.


Country myCountry = Country(
  name: 'United States',
  code: 'US',
  dialCode: '+1',
  flagEmoji: 'πŸ‡ΊπŸ‡Έ',
);

BasicPhoneInput

A ready-to-use phone input widget that provides basic functionality with simple setup.

Parameters

BasicPhoneInput(
  controller: controller,                    // Required: PhoneNumberController
  decoration: InputDecoration(...),          // Optional: Input decoration
  onValidationChanged: (bool isValid) {},   // Optional: Validation callback
  onNumberChanged: (String number) {},      // Optional: Number change callback  
  onCountryChanged: (Country country) {},   // Optional: Country change callback
  showFlag: true,                           // Optional: Show country flag
  showCountryCode: true,                    // Optional: Show dial code
  countryButtonBuilder: (context, country) => CustomWidget(), // Optional: Custom country button
)

AdvancedPhoneInput

A feature-rich phone input widget with advanced customization and validation options.

Parameters

AdvancedPhoneInput(
  controller: controller,                    // Required: PhoneNumberController
  
  // Display Options
  showFlag: true,                           // Show country flag
  showCountryCode: true,                    // Show dial code  
  showCountryName: false,                   // Show country name
  showValidationIcon: true,                 // Show validation status icon
  showPopularCountries: false,              // Show popular countries section
  
  // Functionality
  enableSearch: true,                       // Enable country search
  readOnly: false,                          // Make input read-only
  preferredCountries: [countries],          // List of preferred countries
  
  // Styling
  borderRadius: 8.0,                        // Border radius
  borderColor: Colors.grey,                 // Border color
  focusedBorderColor: Colors.blue,          // Focused border color
  errorBorderColor: Colors.red,             // Error border color
  contentPadding: EdgeInsets.all(12),       // Input padding
  
  // Text & Labels
  hintText: 'Phone number',                 // Placeholder text
  labelText: 'Phone',                       // Label text
  errorText: 'Invalid number',              // Error message
  textStyle: TextStyle(...),                // Input text style
  hintStyle: TextStyle(...),                // Hint text style
  
  // Callbacks
  onValidationChanged: (bool isValid) {},   // Validation status changed
  onCountryChanged: (Country country) {},   // Country selection changed
  onNumberChanged: (String number) {},      // Phone number changed
  
  // Custom Builders
  countryButtonBuilder: (context, country) => Widget, // Custom country selector
  validationIconBuilder: (context, isValid, error) => Widget, // Custom validation icon
)

CountriesData

A utility class containing country data and helper methods with enhanced search capabilities.

Methods

// Core Data Access
List<Country> allCountries = CountriesData.allCountries; // 200+ countries

// Search Methods  
Country getCountryByCode(String code);           // Find by country code (e.g., 'US')
Country getCountryByDialCode(String dialCode);   // Find by dial code (e.g., '+1')
List<Country> searchCountries(String query);     // Search by name, code, or dial code

// Utility Methods
List<Country> getPopularCountries();             // Get commonly used countries

Usage Examples

// Basic lookups
Country us = CountriesData.getCountryByCode('US');
Country uk = CountriesData.getCountryByDialCode('+44');

// Search functionality
List<Country> results = CountriesData.searchCountries('united');
// Returns: [United States, United Kingdom, United Arab Emirates]

// Popular countries for quick access
List<Country> popular = CountriesData.getPopularCountries();
// Returns: [US, UK, CA, AU, DE, FR, IN, CN, JP, BR]

// Safe operations - all methods include error handling
Country fallback = CountriesData.getCountryByCode('INVALID'); // Returns US as fallback

Customization Options

Phone Number Formatting

The package automatically formats phone numbers based on the selected country's standards. You can customize this behavior by modifying the PhoneNumberFormatter class:

class MyCustomFormatter extends PhoneNumberFormatter {
  @override
  String format(String text, String countryCode) {
    // Your custom formatting logic
    return formattedText;
  }
}

Country Picker

You can implement your own country picker UI while still using the package's data:

Future<void> _showCountryPicker() async {
  final selectedCountry = await showModalBottomSheet<Country>(
    context: context,
    builder: (context) {
      return YourCustomCountryPicker(
        countries: CountriesData.allCountries,
        onCountrySelected: (country) {
          Navigator.pop(context, country);
        },
      );
    },
  );

  if (selectedCountry != null) {
    _controller.setCountry(selectedCountry);
  }
}

Adding Your Own Countries

You can extend the country data with your own countries:

// Add your custom countries
List<Country> myCustomCountries = [
  ...CountriesData.allCountries,
  Country(
    name: 'My Country',
    code: 'MC',
    dialCode: '+999',
    flagEmoji: '🏳️',
  ),
];

🎯 Examples & Tutorials

The package includes comprehensive examples for every skill level:

πŸ“š Interactive Examples

Run the example app to see all implementations in action:

cd example
flutter run

The example app includes:

πŸš€ Basic Example - Perfect for Beginners

  • Simple BasicPhoneInput implementation
  • Basic validation and error handling
  • Step-by-step code walkthrough
  • Clear documentation and tips

⚑ Intermediate Example - For Experienced Developers

  • AdvancedPhoneInput with custom styling
  • Form integration and validation
  • Real-time validation feedback
  • Error handling patterns

πŸ† Advanced Example - For Seasoned Professionals

  • Multiple phone controllers
  • Business logic validation
  • Production-ready patterns
  • Complex UI flows with tabs and settings

πŸ”§ Common Use Cases

Form Integration

class RegistrationForm extends StatefulWidget {
  @override
  _RegistrationFormState createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State<RegistrationForm> {
  final _formKey = GlobalKey<FormState>();
  late PhoneNumberController _phoneController;
  String? _phoneError;

  @override
  void initState() {
    super.initState();
    _phoneController = PhoneNumberController();
  }

  String? _validatePhone() {
    if (_phoneController.isEmpty) {
      return 'Phone number is required';
    }
    if (!_phoneController.isValid()) {
      return 'Please enter a valid phone number';
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          // Other form fields...
          
          AdvancedPhoneInput(
            controller: _phoneController,
            errorText: _phoneError,
            onValidationChanged: (isValid) {
              setState(() {
                _phoneError = isValid ? null : 'Invalid phone number';
              });
            },
          ),
          
          ElevatedButton(
            onPressed: () {
              setState(() {
                _phoneError = _validatePhone();
              });
              
              if (_formKey.currentState!.validate() && _phoneError == null) {
                // Submit form
                _submitForm();
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }

  void _submitForm() {
    // Use _phoneController.completeNumber for submission
    print('Submitting: ${_phoneController.completeNumber}');
  }
}

Custom Country Selection

AdvancedPhoneInput(
  controller: _controller,
  preferredCountries: [
    CountriesData.getCountryByCode('US'),
    CountriesData.getCountryByCode('CA'), 
    CountriesData.getCountryByCode('GB'),
  ],
  showPopularCountries: true,
  countryButtonBuilder: (context, country) {
    return Container(
      padding: EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.blue.shade50,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        children: [
          Text(country.flagEmoji, style: TextStyle(fontSize: 20)),
          SizedBox(width: 8),
          Text(country.dialCode, style: TextStyle(fontWeight: FontWeight.bold)),
          Icon(Icons.arrow_drop_down),
        ],
      ),
    );
  },
)

For complete examples with detailed explanations, see the example app.

πŸ› Troubleshooting

Common Issues

Phone number not formatting correctly

// Ensure you're calling formatPhoneNumber after setting the text
controller.setPhoneNumber('1234567890');
controller.formatPhoneNumber(); // This will format based on country

Validation not working

// Make sure to check both isEmpty and isValid
if (controller.isEmpty) {
  print('Please enter a phone number');
} else if (!controller.isValid()) {
  print('Please enter a valid phone number for ${controller.selectedCountry.name}');
}

Performance issues with frequent updates

// The package includes debouncing, but you can also debounce manually
Timer? _debounceTimer;

void _onPhoneChanged() {
  _debounceTimer?.cancel();
  _debounceTimer = Timer(Duration(milliseconds: 300), () {
    // Your validation logic here
  });
}

Performance Tips

  • βœ… Use AdvancedPhoneInput for better performance with large country lists
  • βœ… Set preferredCountries to reduce search time
  • βœ… Enable showPopularCountries for faster country selection
  • βœ… Use proper disposal of controllers to prevent memory leaks

πŸ“‹ Changelog

Version 0.1.0

  • πŸŽ‰ Major Update: Added AdvancedPhoneInput widget
  • ✨ New Features:
    • Enhanced error handling and fallbacks
    • Popular countries support
    • Advanced search functionality
    • Real-time validation with debouncing
    • Custom validation icon builders
    • Preferred countries list
  • πŸ› Bug Fixes:
    • Fixed unresponsiveness issues in basic example
    • Improved cursor positioning during formatting
    • Better error handling for invalid country codes
  • πŸš€ Performance: Debounced validation for better responsiveness
  • πŸ“š Documentation: Added comprehensive examples for all skill levels

Version 0.0.2

  • Basic phone input functionality
  • Country selection and validation
  • Phone number formatting

🀝 Contributing

Contributions are welcome! Here's how you can help:

  1. Report Issues: Found a bug? Open an issue with detailed reproduction steps
  2. Feature Requests: Have an idea? Create a feature request issue
  3. Pull Requests: Want to contribute code? Fork the repo and create a PR
  4. Documentation: Help improve examples and documentation
  5. Testing: Test the package with different countries and edge cases

Development Setup

git clone https://github.com/Syed-Bipul-Rahman/intl_phone_selector.git
cd intl_phone_selector
flutter pub get
cd example
flutter run

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Country data sourced from international telecommunications standards
  • Flag emojis provided by Unicode Consortium