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.
β¨ 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 |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
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:

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:

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:

π 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:
- Report Issues: Found a bug? Open an issue with detailed reproduction steps
- Feature Requests: Have an idea? Create a feature request issue
- Pull Requests: Want to contribute code? Fork the repo and create a PR
- Documentation: Help improve examples and documentation
- 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