Flutter电话号码格式化插件flutter_libphonenumber的使用
Flutter电话号码格式化插件flutter_libphonenumber的使用
插件简介
flutter_libphonenumber
是一个基于 libphonenumber
的包装库,结合了以下两个库的功能:
它使用以下原生库来实现功能:
平台 | 库 | 版本 |
---|---|---|
Android | libphonenumber | 8.13.43 |
iOS | PhoneNumberKit | 3.8.0 |
该库的主要优势在于可以同步格式化电话号码,而无需调用平台方法。
快速开始
初始化
在使用任何格式化功能之前,必须先调用 init()
函数。这将加载设备上所有可用区域,并为每个国家/地区构建格式化掩码。
await init();
如果未调用 init()
函数,则 formatNumberSync
将直接返回传入的值而不进行格式化。
同步格式化电话号码
通常,libphonenumber
的格式化函数是异步的,可能会导致UI频繁重建。为了绕过这个问题,我们可以在 init()
调用时加载每个支持区域的示例号码掩码,然后同步格式化 E164 格式的电话号码:
final rawNumber = '+14145556666';
final formattedNumber = formatNumberSync(rawNumber); // +1 414-555-6666
CountryManager
CountryManager
类用于管理所有国家/地区的电话元数据。每个国家/地区的信息包括:
- 国家代码 (
phoneCode
) - 国家/地区代码 (
countryCode
) - 移动电话示例号码(国内格式)(
exampleNumberMobileNational
) - 固定电话示例号码(国内格式)(
exampleNumberFixedLineNational
) - 移动电话掩码(国内格式)(
phoneMaskMobileNational
) - 固定电话掩码(国内格式)(
phoneMaskFixedLineNational
) - 移动电话示例号码(国际格式)(
exampleNumberMobileInternational
) - 固定电话示例号码(国际格式)(
exampleNumberFixedLineInternational
) - 移动电话掩码(国际格式)(
phoneMaskMobileInternational
) - 固定电话掩码(国际格式)(
phoneMaskFixedLineInternational
) - 国家名称 (
countryName
)
可以通过以下方式访问这些信息:
final countries = CountryManager().countries; // List<CountryWithPhoneCode>
API 参考
Future<void> init({Map<String, CountryWithPhoneCode> overrides})
必须在格式化之前调用此函数。它会加载设备上所有可用的国家/地区,并初始化 CountryWithPhoneCode
列表。可选地提供一个覆盖映射以自定义某些国家/地区的掩码数据。
Future<Map<String, CountryWithPhoneCode>> getAllSupportedRegions()
返回设备上所有可用区域的映射,键为区域代码,值为 CountryWithPhoneCode
对象。
Future<Map<String, String>> format(String phone, String region)
使用 libphonenumber
异步格式化电话号码。返回格式化的号码。
Future<Map<String, dynamic>> parse(String phone, {String? region})
解析电话号码并返回与之关联的元数据。如果号码有效且完整,则返回包含 e164
格式号码和其他信息的映射。
String formatNumberSync(String number, {CountryWithPhoneCode? country, PhoneNumberType phoneNumberType = PhoneNumberType.mobile, PhoneNumberFormat phoneNumberFormat = PhoneNumberFormat.international, bool removeCountryCodeFromResult = false, bool inputContainsCountryCode = true})
同步格式化电话号码。必须先调用 init()
函数以预加载掩码数据。
Future<FormatPhoneResult?> getFormattedParseResult(String phoneNumber, CountryWithPhoneCode country, {PhoneNumberType phoneNumberType = PhoneNumberType.mobile, PhoneNumberFormat phoneNumberFormat = PhoneNumberFormat.international})
异步格式化电话号码并验证其有效性。返回格式化的号码和 e164
格式号码。
TextInputFormatter LibPhonenumberTextFormatter(...)
用于在 TextField
中实时格式化电话号码的文本输入格式化器。
示例代码
以下是一个完整的示例应用程序,展示了如何使用 flutter_libphonenumber
插件:
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_libphonenumber/flutter_libphonenumber.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final phoneController = TextEditingController();
final countryController = TextEditingController(text: 'United States');
final manualFormatController = TextEditingController();
@override
void initState() {
super.initState();
updatePlaceholderHint();
}
/// Result when we call the parse method.
String? parsedData;
/// Used to format numbers as mobile or land line
var _globalPhoneType = PhoneNumberType.mobile;
/// Use international or national phone format
var _globalPhoneFormat = PhoneNumberFormat.international;
/// Current selected country
var _currentSelectedCountry = const CountryWithPhoneCode.us();
var _placeholderHint = '';
var _inputContainsCountryCode = true;
/// Keep cursor on the end
var _shouldKeepCursorAtEndOfInput = true;
void updatePlaceholderHint() {
late String newPlaceholder;
if (_globalPhoneType == PhoneNumberType.mobile) {
if (_globalPhoneFormat == PhoneNumberFormat.international) {
newPlaceholder = _currentSelectedCountry.exampleNumberMobileInternational;
} else {
newPlaceholder = _currentSelectedCountry.exampleNumberMobileNational;
}
} else {
if (_globalPhoneFormat == PhoneNumberFormat.international) {
newPlaceholder = _currentSelectedCountry.exampleNumberFixedLineInternational;
} else {
newPlaceholder = _currentSelectedCountry.exampleNumberFixedLineNational;
}
}
/// Strip country code from hint
if (!_inputContainsCountryCode) {
newPlaceholder = newPlaceholder.substring(_currentSelectedCountry.phoneCode.length + 2);
}
setState(() => _placeholderHint = newPlaceholder);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder<void>(
future: init(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(title: const Text('flutter_libphonenumber')),
body: Center(child: Text('error: ${snapshot.error}')),
);
} else if (snapshot.connectionState == ConnectionState.done) {
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(title: const Text('flutter_libphonenumber')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SingleChildScrollView(
padding: EdgeInsets.only(
bottom: max(0, 24 - MediaQuery.of(context).padding.bottom),
),
child: Column(
children: [
const SizedBox(height: 10),
/// Get all region codes
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Column(
children: [
ElevatedButton(
child: const Text('Print all region data'),
onPressed: () async {
final res = await getAllSupportedRegions();
print(res['IT']);
print(res['US']);
print(res['BR']);
},
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextField(
controller: countryController,
keyboardType: TextInputType.phone,
onChanged: (v) {
setState(() {});
},
textAlign: TextAlign.center,
onTap: () async {
final sortedCountries = CountryManager().countries..sort((a, b) => (a.countryName ?? '').compareTo(b.countryName ?? ''));
final res = await showModalBottomSheet<CountryWithPhoneCode>(
context: context,
isScrollControlled: false,
builder: (context) {
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
itemBuilder: (context, index) {
final item = sortedCountries[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop(item);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
children: [
Expanded(
child: Text('+${item.phoneCode}', textAlign: TextAlign.right),
),
const SizedBox(width: 16),
Expanded(
flex: 8,
child: Text(item.countryName ?? ''),
),
],
),
),
);
},
itemCount: sortedCountries.length,
);
},
);
print('New country selection: $res');
if (res != null) {
setState(() {
_currentSelectedCountry = res;
});
updatePlaceholderHint();
countryController.text = res.countryName ?? '+ ${res.phoneCode}';
}
},
readOnly: true,
inputFormatters: const [],
),
),
],
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
children: [
Row(
children: [
Switch(
value: _globalPhoneType == PhoneNumberType.mobile,
onChanged: (val) {
setState(() => _globalPhoneType = val ? PhoneNumberType.mobile : PhoneNumberType.fixedLine);
updatePlaceholderHint();
},
),
const SizedBox(width: 5),
Flexible(
child: _globalPhoneType == PhoneNumberType.mobile ? const Text('Format as Mobile') : const Text('Format as FixedLine'),
),
],
),
Row(
children: [
Switch(
value: _globalPhoneFormat == PhoneNumberFormat.national,
onChanged: (val) {
setState(() => _globalPhoneFormat = val ? PhoneNumberFormat.national : PhoneNumberFormat.international);
updatePlaceholderHint();
},
),
const SizedBox(width: 5),
Flexible(
child: _globalPhoneFormat == PhoneNumberFormat.national ? const Text('National') : const Text('International'),
),
],
),
Row(
children: [
Switch(
value: _inputContainsCountryCode,
onChanged: (val) {
setState(() => _inputContainsCountryCode = !_inputContainsCountryCode);
updatePlaceholderHint();
},
),
const SizedBox(width: 5),
Flexible(
child: _inputContainsCountryCode ? const Text('With country code') : const Text('No country code'),
),
],
),
Row(
children: [
Switch(
value: _shouldKeepCursorAtEndOfInput,
onChanged: (val) {
setState(() => _shouldKeepCursorAtEndOfInput = !_shouldKeepCursorAtEndOfInput);
updatePlaceholderHint();
},
),
const SizedBox(width: 5),
const Flexible(
child: Text('Force cursor to end'),
),
],
),
],
),
),
],
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 10),
const Text('Format as you type (synchronous using masks)'),
SizedBox(
width: 160,
child: TextField(
textAlign: TextAlign.center,
keyboardType: TextInputType.phone,
controller: phoneController,
decoration: InputDecoration(hintText: _placeholderHint),
inputFormatters: [
LibPhonenumberTextFormatter(
phoneNumberType: _globalPhoneType,
phoneNumberFormat: _globalPhoneFormat,
country: _currentSelectedCountry,
inputContainsCountryCode: _inputContainsCountryCode,
shouldKeepCursorAtEndOfInput: _shouldKeepCursorAtEndOfInput,
),
],
),
),
const SizedBox(height: 10),
const Text(
'If country code is not empty, phone number will format expecting no country code.',
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
const Text(
'Manually format / parse the phone number.\nAsync uses FlutterLibphonenumber().format().\nSync uses FlutterLibphonenumber().formatPhone.',
style: TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
SizedBox(
width: 180,
child: TextField(
keyboardType: TextInputType.phone,
textAlign: TextAlign.center,
controller: manualFormatController,
decoration: InputDecoration(hintText: _placeholderHint),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: ElevatedButton(
child: const Text('Format (Async)', textAlign: TextAlign.center),
onPressed: () async {
final res = await format(manualFormatController.text, _currentSelectedCountry.countryCode);
setState(() => manualFormatController.text = res['formatted'] ?? '');
},
),
),
const SizedBox(width: 10),
Flexible(
child: ElevatedButton(
child: const Text('Format (Sync)', textAlign: TextAlign.center),
onPressed: () async {
if (CountryManager().countries.isEmpty) {
print("Warning: countries list is empty which means init has not been run yet. Can't format synchronously until init has been executed.");
}
manualFormatController.text = formatNumberSync(
manualFormatController.text,
country: _currentSelectedCountry,
phoneNumberType: _globalPhoneType,
phoneNumberFormat: _globalPhoneFormat,
inputContainsCountryCode: _inputContainsCountryCode,
);
},
),
),
const SizedBox(width: 10),
Flexible(
child: ElevatedButton(
child: const Text('Parse', textAlign: TextAlign.center),
onPressed: () async {
try {
final res = await parse(manualFormatController.text, region: _currentSelectedCountry.countryCode);
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
setState(() => parsedData = encoder.convert(res));
} catch (e) {
print(e);
setState(() => parsedData = null);
}
},
),
),
],
),
const SizedBox(height: 10),
Text(parsedData ?? 'Number invalid'),
],
),
),
),
),
);
} else {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(title: const Text('flutter_libphonenumber')),
body: const Center(child: CircularProgressIndicator()),
);
}
},
),
);
}
}
这个示例应用程序展示了如何使用 flutter_libphonenumber
插件进行电话号码的格式化和解析。它包括同步和异步格式化、手动格式化、解析以及实时格式化等功能。
更多关于Flutter电话号码格式化插件flutter_libphonenumber的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter电话号码格式化插件flutter_libphonenumber的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用flutter_libphonenumber
插件进行电话号码格式化的代码示例。flutter_libphonenumber
是一个强大的库,它基于Google的libphonenumber
库,可以方便地处理和格式化国际电话号码。
1. 添加依赖
首先,在你的pubspec.yaml
文件中添加flutter_libphonenumber
依赖:
dependencies:
flutter:
sdk: flutter
flutter_libphonenumber: ^x.y.z # 请替换为最新版本号
然后运行flutter pub get
来安装依赖。
2. 导入插件
在你的Dart文件中导入flutter_libphonenumber
:
import 'package:flutter_libphonenumber/flutter_libphonenumber.dart';
3. 使用插件格式化电话号码
以下是一个完整的示例,展示了如何使用flutter_libphonenumber
来解析和格式化电话号码:
import 'package:flutter/material.dart';
import 'package:flutter_libphonenumber/flutter_libphonenumber.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Phone Number Formatting'),
),
body: Center(
child: PhoneNumberFormattingExample(),
),
),
);
}
}
class PhoneNumberFormattingExample extends StatefulWidget {
@override
_PhoneNumberFormattingExampleState createState() => _PhoneNumberFormattingExampleState();
}
class _PhoneNumberFormattingExampleState extends State<PhoneNumberFormattingExample> {
final TextEditingController _controller = TextEditingController();
String _formattedNumber = '';
void _parseAndFormatPhoneNumber() async {
try {
PhoneNumberNumberFormat format = PhoneNumberFormat.INTERNATIONAL;
String number = _controller.text;
PhoneNumber parseResult = await PhoneNumberUtil.getInstance().parse(number, "US"); // 假设默认国家代码为US
if (parseResult.isValidNumber()) {
String formattedNumber = await PhoneNumberUtil.getInstance().format(parseResult, format);
setState(() {
_formattedNumber = formattedNumber;
});
} else {
setState(() {
_formattedNumber = 'Invalid number';
});
}
} catch (e) {
setState(() {
_formattedNumber = 'Error: ${e.message}';
});
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Enter Phone Number',
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _parseAndFormatPhoneNumber,
child: Text('Format Number'),
),
SizedBox(height: 20),
Text(
'Formatted Number: $_formattedNumber',
style: TextStyle(fontSize: 18),
),
],
);
}
}
4. 运行应用
将上述代码添加到你的Flutter项目中,然后运行应用。你将看到一个简单的界面,允许你输入一个电话号码,点击按钮后,该号码将根据国际格式进行格式化并显示。
注意事项
- 在
PhoneNumberUtil.getInstance().parse
方法中,第二个参数是默认的国家代码(这里假设为US
)。你可以根据应用的需求调整这个参数。 PhoneNumberNumberFormat
有多种格式选项,如E164
、NATIONAL
、RFC3966
等,你可以根据需要选择合适的格式。
这样,你就可以使用flutter_libphonenumber
插件在Flutter应用中实现电话号码的格式化功能了。