Flutter电话号码格式化插件flutter_libphonenumber的使用

发布于 1周前 作者 songsunli 来自 Flutter

Flutter电话号码格式化插件flutter_libphonenumber的使用

插件简介

flutter_libphonenumber 是一个基于 libphonenumber 的包装库,结合了以下两个库的功能:

它使用以下原生库来实现功能:

平台 版本
Android libphonenumber 8.13.43
iOS PhoneNumberKit 3.8.0

该库的主要优势在于可以同步格式化电话号码,而无需调用平台方法。

AsYouType实时格式化 格式化和解析

快速开始

初始化

在使用任何格式化功能之前,必须先调用 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

1 回复

更多关于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有多种格式选项,如E164NATIONALRFC3966等,你可以根据需要选择合适的格式。

这样,你就可以使用flutter_libphonenumber插件在Flutter应用中实现电话号码的格式化功能了。

回到顶部