基础入门 Flutter for OpenHarmony:三方库实战 flutter_phone_direct_caller 电话拨号详解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 flutter_phone_direct_caller 电话拨号插件的使用方法,带你全面掌握在应用中直接拨打电话的功能。


一、flutter_phone_direct_caller 组件概述

在很多应用中,需要提供拨打电话的功能,例如联系客服、拨打紧急电话、一键呼叫等。flutter_phone_direct_caller 是一个简单易用的电话拨号插件,可以直接调用系统拨号功能,无需打开拨号界面。

📋 flutter_phone_direct_caller 组件特点

特点 说明
直接拨号 直接拨打电话,无需经过拨号界面
权限自动处理 插件自动处理权限请求
跨平台支持 支持 Android、iOS、OpenHarmony
简单易用 API 简洁,一行代码即可拨打电话
鸿蒙适配 专门为 OpenHarmony 平台进行了适配

二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目基于 flutter_phone_direct_caller@2.2.1 开发,适配 Flutter 3.7.12-ohos-1.0.6 和 Flutter 3.22.1-ohos-1.0.1。

2.2 工作原理

flutter_phone_direct_caller 在 OpenHarmony 平台上使用以下机制:

  1. Call 能力:使用 OpenHarmony 的 Call 能力进行电话拨号
  2. 权限管理:插件自动处理拨号权限请求
  3. MethodChannel 通信:通过 Flutter MethodChannel 调用原生拨号接口
详细工作流程

Call API OpenHarmony Native Layer MethodChannel Dart Layer Call API OpenHarmony Native Layer MethodChannel Dart Layer 1. 用户点击拨打电话按钮 2. 传递拨号请求 3. 检查权限 5. 等待用户授权 alt [用户授权] [用户拒绝] alt [权限已授予] [权限未授予] 7. 处理拨号结果 callNumber(phoneNumber) 方法调用 4. 调用 Call API makeCall(phoneNumber) 拨号结果 4. 请求权限 6. 调用 Call API makeCall(phoneNumber) 拨号结果 6. 返回失败 返回结果(true/false) Future<bool>

OpenHarmony 平台实现

在 OpenHarmony 平台上,flutter_phone_direct_caller 通过以下方式实现:

dart 复制代码
// OpenHarmony 原生代码(伪代码)
public class FlutterPhoneDirectCallerPlugin implements MethodCallHandler {
    private static final String CHANNEL = "flutter_phone_direct_caller/methods";
  
    @Override
    public void onMethodCall(MethodCall call, Result result) {
        if (call.method.equals("callNumber")) {
            String phoneNumber = call.argument("number");
            callNumber(phoneNumber, result);
        } else {
            result.notImplemented();
        }
    }
  
    private void callNumber(String phoneNumber, Result result) {
        try {
            // 1. 检查并请求权限
            if (!checkCallPermission()) {
                requestCallPermission((isGranted) -> {
                    if (isGranted) {
                        performCall(phoneNumber, result);
                    } else {
                        result.success(false);
                    }
                });
                return;
            }
    
            // 2. 执行拨号
            performCall(phoneNumber, result);
    
        } catch (Exception e) {
            result.error("CALL_ERROR", e.getMessage(), null);
        }
    }
  
    private void performCall(String phoneNumber, Result result) {
        // 使用 OpenHarmony 的 Call 能力
        CallAbility callAbility = new CallAbility(context);
        callAbility.makeCall(phoneNumber, new CallCallback() {
            @Override
            public void onSuccess() {
                result.success(true);
            }
    
            @Override
            public void onFailure(String error) {
                result.success(false);
            }
        });
    }
  
    private boolean checkCallPermission() {
        // 检查是否有拨号权限
        return verifySelfPermission("ohos.permission.CALL_PHONE") == 
            PackageManager.PERMISSION_GRANTED;
    }
  
    private void requestCallPermission(PermissionCallback callback) {
        // 请求拨号权限
        requestPermissionsFromUser(
            new String[]{"ohos.permission.CALL_PHONE"},
            REQUEST_CODE_CALL_PHONE,
            (code, data) -> {
                boolean isGranted = verifySelfPermission("ohos.permission.CALL_PHONE") == 
                    PackageManager.PERMISSION_GRANTED;
                callback.onResult(isGranted);
            }
        );
    }
}

代码说明:这段伪代码展示了 OpenHarmony 原生层的实现逻辑。首先通过 MethodChannel 接收 Flutter 层的拨号请求,然后检查拨号权限。如果权限已授予,直接调用 Call API 拨打电话;如果权限未授予,则先请求用户授权,根据用户授权结果决定是否拨号。最后将拨号结果返回给 Flutter 层。

Dart 层实现

在 Dart 层,flutter_phone_direct_caller 的实现如下:

dart 复制代码
import 'package:flutter/services.dart';

class FlutterPhoneDirectCaller {
  static const MethodChannel _channel = MethodChannel('flutter_phone_direct_caller/methods');

  /// 拨打电话
  /// 
  /// [number] 要拨打的电话号码
  /// 
  /// 返回 [Future<bool>],true 表示拨号成功,false 表示拨号失败
  static Future<bool> callNumber(String number) async {
    try {
      // 通过 MethodChannel 调用原生方法
      final result = await _channel.invokeMethod('callNumber', {'number': number});
  
      // 原生层返回 bool 类型结果
      return result as bool;
    } on PlatformException catch (e) {
      // 处理平台异常
      print('拨号失败: ${e.message}');
      return false;
    } catch (e) {
      // 处理其他异常
      print('拨号失败: $e');
      return false;
    }
  }
}

代码说明:Dart 层使用 MethodChannel 与原生层通信。callNumber 方法接收电话号码参数,通过 invokeMethod 调用原生的 callNumber 方法,并将参数传递过去。使用 try-catch 捕获可能出现的异常,确保调用失败时返回 false 而不是抛出异常,方便调用者处理。

2.3 支持的功能

功能 说明 OpenHarmony 支持
直接拨号 直接拨打指定号码 ✅ yes
返回结果 返回拨号结果 ✅ yes

三、项目配置与安装

3.1 添加依赖配置

pubspec.yaml 文件中添加以下依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  # 添加 flutter_phone_direct_caller 依赖(OpenHarmony 适配版本)
  flutter_phone_direct_caller:
    git:
      url: https://gitcode.com/openharmony-sig/fluttertpc_flutter_phone_direct_caller.git

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的 flutter_phone_direct_caller 仓库
  • url:指定 GitCode 托管的仓库地址
  • 无需指定 path,因为这是根目录包

3.2 下载依赖

配置完成后,执行以下命令下载依赖:

bash 复制代码
flutter pub get

3.3 权限配置

flutter_phone_direct_caller 会自动处理权限,无需手动配置。


四、flutter_phone_direct_caller 基础用法

4.1 导入库

在使用电话拨号之前,需要先导入库:

dart 复制代码
import 'package:flutter_phone_direct_caller/flutter_phone_direct_caller.dart';

4.2 拨打电话

使用 callNumber 方法拨打电话:

dart 复制代码
// 拨打电话
bool result = await FlutterPhoneDirectCaller.callNumber('085921191121');

print('拨号结果: $result');

参数说明:

  • number:要拨打的电话号码(字符串)
  • 返回值:bool
    • true:拨号成功
    • false:拨号失败

4.3 格式化电话号码

在拨打电话之前,建议对电话号码进行格式化:

dart 复制代码
String formatPhoneNumber(String phone) {
  // 移除所有非数字字符
  return phone.replaceAll(RegExp(r'[^\d]'), '');
}

五、实际应用场景

5.1 联系客服

dart 复制代码
class CustomerServiceWidget extends StatelessWidget {
  final String servicePhone = '4008888888';
  final String serviceTime = '09:00-18:00';

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.blue.shade100,
          child: const Icon(Icons.headset_mic, color: Colors.blue),
        ),
        title: const Text('联系客服'),
        subtitle: Text('服务时间: $serviceTime'),
        trailing: const Icon(Icons.phone, color: Colors.blue),
        onTap: () async {
          // 弹出确认对话框
          final shouldCall = await showDialog<bool>(
            context: context,
            builder: (context) => AlertDialog(
              title: const Text('联系客服'),
              content: Text('确定要拨打客服电话 $servicePhone 吗?\n服务时间: $serviceTime'),
              actions: [
                TextButton(
                  onPressed: () => Navigator.pop(context, false),
                  child: const Text('取消'),
                ),
                TextButton(
                  onPressed: () => Navigator.pop(context, true),
                  child: const Text('拨打'),
                ),
              ],
            ),
          );

          if (shouldCall == true) {
            final result = await FlutterPhoneDirectCaller.callNumber(servicePhone);
            if (!result) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('拨号失败,请检查设备是否支持通话功能')),
              );
            }
          }
        },
      ),
    );
  }
}

代码说明:这个示例创建了一个客服联系卡片组件,包含客服图标、服务时间等信息。当用户点击时,首先显示确认对话框,确认后才发起拨号,避免误操作。如果拨号失败,会通过 SnackBar 提示用户。

5.2 紧急呼叫

dart 复制代码
class EmergencyCallWidget extends StatelessWidget {
  final Map<String, String> emergencyNumbers = {
    '110': '报警电话',
    '120': '急救电话',
    '119': '火警电话',
    '122': '交通事故',
  };

  @override
  Widget build(BuildContext context) {
    return Card(
      color: Colors.red.shade50,
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
        side: BorderSide(color: Colors.red.shade300),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Icon(Icons.warning, color: Colors.red),
                SizedBox(width: 8),
                Text(
                  '紧急呼叫',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
          ),
          ...emergencyNumbers.entries.map((entry) => ListTile(
            leading: CircleAvatar(
              backgroundColor: Colors.red.shade100,
              child: Text(entry.key, style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
            ),
            title: Text(entry.value),
            trailing: const Icon(Icons.phone_in_talk, color: Colors.red),
            onTap: () async {
              final shouldCall = await showDialog<bool>(
                context: context,
                builder: (context) => AlertDialog(
                  title: const Text('紧急呼叫'),
                  content: Text('确定要拨打 ${entry.value} (${entry.key}) 吗?'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context, false),
                      child: const Text('取消'),
                    ),
                    ElevatedButton(
                      onPressed: () => Navigator.pop(context, true),
                      style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                      child: const Text('紧急拨打'),
                    ),
                  ],
                ),
              );

              if (shouldCall == true) {
                final result = await FlutterPhoneDirectCaller.callNumber(entry.key);
                if (!result) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('拨号失败,请检查设备是否支持通话功能')),
                  );
                }
              }
            },
          )),
        ],
      ),
    );
  }
}

代码说明:这个示例创建了紧急呼叫组件,包含多个紧急电话号码(110、120、119、122)。使用红色主题突出紧急性,每个号码都有明确的标识和说明。点击时会弹出确认对话框,确认后立即拨打电话。

5.3 电话号码管理

dart 复制代码
class PhoneNumberManagerWidget extends StatelessWidget {
  final List<PhoneNumberEntry> phoneNumbers;

  const PhoneNumberManagerWidget({Key? key, required this.phoneNumbers}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              '联系电话',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
          ),
          ...phoneNumbers.asMap().entries.map((entry) {
            final index = entry.key;
            final phone = entry.value;
            return ListTile(
              leading: CircleAvatar(
                backgroundColor: _getPhoneTypeColor(phone.type).shade100,
                child: Icon(_getPhoneTypeIcon(phone.type), color: _getPhoneTypeColor(phone.type)),
              ),
              title: Text(phone.number),
              subtitle: Text(phone.label),
              trailing: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton(
                    icon: const Icon(Icons.copy, size: 20),
                    onPressed: () {
                      Clipboard.setData(ClipboardData(text: phone.number));
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('号码已复制')),
                      );
                    },
                  ),
                  IconButton(
                    icon: const Icon(Icons.phone, color: Colors.green),
                    onPressed: () async {
                      final result = await FlutterPhoneDirectCaller.callNumber(phone.number);
                      if (result) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('正在拨打 ${phone.label}...')),
                        );
                      } else {
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(content: Text('拨号失败')),
                        );
                      }
                    },
                  ),
                ],
              ),
            );
          }),
        ],
      ),
    );
  }

  Color _getPhoneTypeColor(PhoneType type) {
    switch (type) {
      case PhoneType.mobile:
        return Colors.blue;
      case PhoneType.home:
        return Colors.green;
      case PhoneType.work:
        return Colors.orange;
      case PhoneType.other:
        return Colors.grey;
    }
  }

  IconData _getPhoneTypeIcon(PhoneType type) {
    switch (type) {
      case PhoneType.mobile:
        return Icons.smartphone;
      case PhoneType.home:
        return Icons.home;
      case PhoneType.work:
        return Icons.work;
      case PhoneType.other:
        return Icons.phone;
    }
  }
}

class PhoneNumberEntry {
  final String number;
  final String label;
  final PhoneType type;

  PhoneNumberEntry({
    required this.number,
    required this.label,
    required this.type,
  });
}

enum PhoneType { mobile, home, work, other }

代码说明:这个示例创建了电话号码管理组件,可以管理多个不同类型的电话号码(手机、家庭、工作等)。每个号码都有对应的图标和颜色标识,方便区分。提供复制号码和拨打电话两个功能。


六、高级用法

6.1 电话号码验证与格式化

在实际应用中,用户输入的电话号码可能格式不正确,需要进行验证和格式化。

dart 复制代码
class PhoneNumberValidator {
  /// 验证手机号码格式(中国大陆)
  static bool isValidMobilePhone(String phone) {
    // 移除所有非数字字符
    final cleanPhone = phone.replaceAll(RegExp(r'[^\d]'), '');
  
    // 验证11位数字,以1开头
    final regex = RegExp(r'^1[3-9]\d{9}$');
    return regex.hasMatch(cleanPhone);
  }

  /// 验证固定电话格式
  static bool isValidLandline(String phone) {
    final cleanPhone = phone.replaceAll(RegExp(r'[^\d]'), '');
  
    // 验证固定电话格式:区号(3-4位) + 号码(7-8位)
    final regex = RegExp(r'^0\d{2,3}\d{7,8}$');
    return regex.hasMatch(cleanPhone);
  }

  /// 验证国际电话格式
  static bool isValidInternational(String phone) {
    final cleanPhone = phone.replaceAll(RegExp(r'[^\d+]'), '');
  
    // 验证国际电话格式:国家代码(1-3位) + 号码
    final regex = RegExp(r'^\+[1-9]\d{0,2}\d{6,14}$');
    return regex.hasMatch(cleanPhone);
  }

  /// 格式化手机号码为 138 1234 5678 格式
  static String formatMobilePhone(String phone) {
    final cleanPhone = phone.replaceAll(RegExp(r'[^\d]'), '');
    if (cleanPhone.length == 11) {
      return '${cleanPhone.substring(0, 3)} ${cleanPhone.substring(3, 7)} ${cleanPhone.substring(7)}';
    }
    return phone;
  }

  /// 格式化固定电话
  static String formatLandline(String phone) {
    final cleanPhone = phone.replaceAll(RegExp(r'[^\d]'), '');
    if (cleanPhone.length >= 10 && cleanPhone.length <= 12) {
      // 区号 + 号码
      final areaCodeLength = cleanPhone.length == 10 ? 3 : 4;
      return '${cleanPhone.substring(0, areaCodeLength)}-${cleanPhone.substring(areaCodeLength)}';
    }
    return phone;
  }

  /// 格式化国际电话
  static String formatInternational(String phone) {
    final cleanPhone = phone.replaceAll(RegExp(r'[^\d+]'), '');
    if (cleanPhone.startsWith('+') && cleanPhone.length > 12) {
      return '${cleanPhone.substring(0, cleanPhone.length - 8)} ${cleanPhone.substring(cleanPhone.length - 8)}';
    }
    return phone;
  }
}

代码说明:PhoneNumberValidator 类提供了电话号码验证和格式化的方法。包括手机号码验证(中国大陆)、固定电话验证、国际电话验证,以及对应的格式化方法。格式化方法可以增强用户体验,使电话号码更易读。

6.2 拨号历史记录

记录用户的拨号历史,方便快速回拨和查看通话记录。

dart 复制代码
class CallHistoryManager {
  static final CallHistoryManager _instance = CallHistoryManager._internal();
  factory CallHistoryManager() => _instance;
  CallHistoryManager._internal();

  final List<CallRecord> _history = [];

  List<CallRecord> get history => List.unmodifiable(_history);

  /// 添加拨号记录
  void addRecord(String phoneNumber, {String? name}) {
    final record = CallRecord(
      phoneNumber: phoneNumber,
      name: name ?? phoneNumber,
      timestamp: DateTime.now(),
    );
  
    _history.insert(0, record);
  
    // 只保留最近100条记录
    if (_history.length > 100) {
      _history.removeLast();
    }
  }

  /// 获取指定电话的拨打次数
  int getCallCount(String phoneNumber) {
    return _history.where((r) => r.phoneNumber == phoneNumber).length;
  }

  /// 获取最后一次拨打时间
  DateTime? getLastCallTime(String phoneNumber) {
    final record = _history.firstWhere(
      (r) => r.phoneNumber == phoneNumber,
      orElse: () => CallRecord(phoneNumber: phoneNumber, name: phoneNumber, timestamp: DateTime(1970)),
    );
  
    if (record.timestamp.year > 2000) {
      return record.timestamp;
    }
    return null;
  }

  /// 清除所有记录
  void clearAll() {
    _history.clear();
  }

  /// 删除指定记录
  void deleteRecord(CallRecord record) {
    _history.remove(record);
  }
}

class CallRecord {
  final String phoneNumber;
  final String name;
  final DateTime timestamp;

  CallRecord({
    required this.phoneNumber,
    required this.name,
    required this.timestamp,
  });

  Map<String, dynamic> toJson() {
    return {
      'phoneNumber': phoneNumber,
      'name': name,
      'timestamp': timestamp.toIso8601String(),
    };
  }

  factory CallRecord.fromJson(Map<String, dynamic> json) {
    return CallRecord(
      phoneNumber: json['phoneNumber'],
      name: json['name'],
      timestamp: DateTime.parse(json['timestamp']),
    );
  }
}

// 使用示例
class CallHistoryWidget extends StatelessWidget {
  final CallHistoryManager _historyManager = CallHistoryManager();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('拨号历史'),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () {
              _historyManager.clearAll();
            },
          ),
        ],
      ),
      body: Obx(() {
        final history = _historyManager.history;
        if (history.isEmpty) {
          return const Center(child: Text('暂无拨号记录'));
        }
  
        return ListView.builder(
          itemCount: history.length,
          itemBuilder: (context, index) {
            final record = history[index];
            return ListTile(
              leading: CircleAvatar(
                child: Text(record.name[0]),
              ),
              title: Text(record.name),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(record.phoneNumber),
                  Text(
                    _formatTime(record.timestamp),
                    style: const TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                ],
              ),
              trailing: IconButton(
                icon: const Icon(Icons.phone, color: Colors.green),
                onPressed: () async {
                  final result = await FlutterPhoneDirectCaller.callNumber(record.phoneNumber);
                  if (result) {
                    _historyManager.addRecord(record.phoneNumber, name: record.name);
                  }
                },
              ),
            );
          },
        );
      }),
    );
  }

  String _formatTime(DateTime time) {
    final now = DateTime.now();
    final difference = now.difference(time);
  
    if (difference.inDays > 0) {
      return '${difference.inDays}天前';
    } else if (difference.inHours > 0) {
      return '${difference.inHours}小时前';
    } else if (difference.inMinutes > 0) {
      return '${difference.inMinutes}分钟前';
    } else {
      return '刚刚';
    }
  }
}

代码说明:CallHistoryManager 类管理用户的拨号历史记录。支持添加记录、获取拨打次数、获取最后拨打时间、清除记录等功能。CallHistoryWidget 展示拨号历史列表,支持快速回拨。历史记录可以持久化存储到本地,以便应用重启后恢复。

6.3 拨号确认对话框

在拨打电话前显示详细信息,避免误拨,并提供更多信息展示。

dart 复制代码
class CallConfirmationDialog {
  /// 显示拨号确认对话框
  static Future<bool> show(
    BuildContext context, {
    required String phoneNumber,
    String? name,
    String? description,
    bool isEmergency = false,
  }) async {
    return await showDialog<bool>(
      context: context,
      builder: (context) => _CallConfirmationDialog(
        phoneNumber: phoneNumber,
        name: name,
        description: description,
        isEmergency: isEmergency,
      ),
    ) ?? false;
  }
}

class _CallConfirmationDialog extends StatelessWidget {
  final String phoneNumber;
  final String? name;
  final String? description;
  final bool isEmergency;

  const _CallConfirmationDialog({
    required this.phoneNumber,
    this.name,
    this.description,
    required this.isEmergency,
  });

  @override
  Widget build(BuildContext context) {
    final theme = isEmergency ? Colors.red : Theme.of(context).primaryColor;
  
    return AlertDialog(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      title: Row(
        children: [
          Icon(
            isEmergency ? Icons.warning : Icons.phone,
            color: theme,
          ),
          const SizedBox(width: 8),
          Text(isEmergency ? '紧急呼叫' : '确认拨号'),
        ],
      ),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (name != null) ...[
            Text(
              name!,
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
          ],
          Text(
            phoneNumber,
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
              color: theme,
            ),
          ),
          if (description != null) ...[
            const SizedBox(height: 12),
            Text(
              description!,
              style: const TextStyle(color: Colors.grey),
            ),
          ],
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context, true),
          style: ElevatedButton.styleFrom(
            backgroundColor: isEmergency ? Colors.red : null,
          ),
          child: const Text('拨打'),
        ),
      ],
    );
  }
}

// 使用示例
class SmartCallButton extends StatelessWidget {
  final String phoneNumber;
  final String? name;
  final String? description;
  final bool isEmergency;

  const SmartCallButton({
    Key? key,
    required this.phoneNumber,
    this.name,
    this.description,
    this.isEmergency = false,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton.icon(
      icon: Icon(
        isEmergency ? Icons.phone_in_talk : Icons.phone,
        color: isEmergency ? Colors.white : null,
      ),
      label: Text(isEmergency ? '紧急呼叫' : '拨打'),
      style: ElevatedButton.styleFrom(
        backgroundColor: isEmergency ? Colors.red : null,
      ),
      onPressed: () async {
        final shouldCall = await CallConfirmationDialog.show(
          context,
          phoneNumber: phoneNumber,
          name: name,
          description: description,
          isEmergency: isEmergency,
        );
  
        if (shouldCall) {
          final result = await FlutterPhoneDirectCaller.callNumber(phoneNumber);
          if (!result) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('拨号失败')),
            );
          }
        }
      },
    );
  }
}

代码说明:CallConfirmationDialog 类提供了功能丰富的拨号确认对话框,可以显示联系人名称、描述信息。支持紧急呼叫模式,使用红色主题突出紧急性。SmartCallButton 封装了拨号按钮和确认对话框,提供统一的拨号体验。

6.4 常用联系人管理

管理常用联系人,提供快速拨号功能。

dart 复制代码
class FavoriteContactsManager {
  static final FavoriteContactsManager _instance = FavoriteContactsManager._internal();
  factory FavoriteContactsManager() => _instance;
  FavoriteContactsManager._internal();

  final List<Contact> _favorites = [];

  List<Contact> get favorites => List.unmodifiable(_favorites);

  /// 添加常用联系人
  void addContact(Contact contact) {
    if (!_favorites.any((c) => c.phone == contact.phone)) {
      _favorites.add(contact);
    }
  }

  /// 移除常用联系人
  void removeContact(Contact contact) {
    _favorites.remove(contact);
  }

  /// 更新联系人
  void updateContact(Contact oldContact, Contact newContact) {
    final index = _favorites.indexWhere((c) => c.phone == oldContact.phone);
    if (index != -1) {
      _favorites[index] = newContact;
    }
  }
}

class Contact {
  final String name;
  final String phone;
  final String? avatar;
  final ContactType type;

  Contact({
    required this.name,
    required this.phone,
    this.avatar,
    this.type = ContactType.other,
  });
}

enum ContactType { family, friend, work, other }

class FavoriteContactsWidget extends StatelessWidget {
  final FavoriteContactsManager _manager = FavoriteContactsManager();

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  '常用联系人',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                TextButton.icon(
                  icon: const Icon(Icons.add, size: 18),
                  label: const Text('添加'),
                  onPressed: () {
                    // 添加联系人
                  },
                ),
              ],
            ),
            const SizedBox(height: 12),
            if (_manager.favorites.isEmpty)
              const Padding(
                padding: EdgeInsets.all(24),
                child: Center(
                  child: Text(
                    '暂无常用联系人',
                    style: TextStyle(color: Colors.grey),
                  ),
                ),
              )
            else
              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: _manager.favorites.map((contact) {
                  return InkWell(
                    onTap: () async {
                      final shouldCall = await CallConfirmationDialog.show(
                        context,
                        phoneNumber: contact.phone,
                        name: contact.name,
                      );
              
                      if (shouldCall) {
                        await FlutterPhoneDirectCaller.callNumber(contact.phone);
                      }
                    },
                    child: Container(
                      width: 80,
                      padding: const EdgeInsets.all(8),
                      decoration: BoxDecoration(
                        color: Colors.grey.shade100,
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: Column(
                        children: [
                          CircleAvatar(
                            backgroundImage: contact.avatar != null
                                ? NetworkImage(contact.avatar!)
                                : null,
                            child: contact.avatar == null
                                ? Text(contact.name[0])
                                : null,
                          ),
                          const SizedBox(height: 4),
                          Text(
                            contact.name,
                            style: const TextStyle(fontSize: 12),
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                          ),
                        ],
                      ),
                    ),
                  );
                }).toList(),
              ),
          ],
        ),
      ),
    );
  }
}

代码说明:FavoriteContactsManager 类管理常用联系人列表。支持添加、删除、更新联系人。FavoriteContactsWidget 以网格布局展示常用联系人,点击联系人后显示确认对话框,确认后拨打电话。常用联系人可以持久化存储到本地。


七、完整示例代码

下面是一个完整的电话拨号示例应用:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_phone_direct_caller/flutter_phone_direct_caller.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '电话拨号示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
        useMaterial3: true,
      ),
      home: const PhoneCallerPage(),
    );
  }
}

class PhoneCallerPage extends StatefulWidget {
  const PhoneCallerPage({Key? key}) : super(key: key);

  @override
  State<PhoneCallerPage> createState() => _PhoneCallerPageState();
}

class _PhoneCallerPageState extends State<PhoneCallerPage> {
  final TextEditingController _numberController = TextEditingController();
  String? _result;
  bool _isCalling = false;

  @override
  void initState() {
    super.initState();
    _numberController.text = '085921191121';
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('电话拨号示例'),
        backgroundColor: Colors.green,
      ),
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.green.shade50,
              Colors.blue.shade50,
            ],
          ),
        ),
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: Card(
              elevation: 8,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(16),
              ),
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Icon(
                      Icons.phone_in_talk,
                      size: 64,
                      color: Colors.green,
                    ),
                    const SizedBox(height: 24),
                    const Text(
                      '电话拨号',
                      style: TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 32),
                    TextField(
                      controller: _numberController,
                      keyboardType: TextInputType.phone,
                      decoration: const InputDecoration(
                        labelText: '电话号码',
                        hintText: '请输入要拨打的电话号码',
                        prefixIcon: Icon(Icons.phone),
                        border: OutlineInputBorder(),
                      ),
                    ),
                    const SizedBox(height: 24),
                    ElevatedButton.icon(
                      icon: _isCalling
                          ? const SizedBox(
                              width: 20,
                              height: 20,
                              child: CircularProgressIndicator(
                                strokeWidth: 2,
                                color: Colors.white,
                              ),
                            )
                          : const Icon(Icons.call),
                      label: Text(_isCalling ? '正在拨号...' : '拨打电话'),
                      onPressed: _isCalling ? null : _makeCall,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.green,
                        foregroundColor: Colors.white,
                        minimumSize: const Size.fromHeight(56),
                      ),
                    ),
                    const SizedBox(height: 16),
                    if (_result != null)
                      Container(
                        padding: const EdgeInsets.all(12),
                        decoration: BoxDecoration(
                          color: _result == 'true'
                              ? Colors.green.shade100
                              : Colors.red.shade100,
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: Row(
                          children: [
                            Icon(
                              _result == 'true'
                                  ? Icons.check_circle
                                  : Icons.error,
                              color: _result == 'true'
                                  ? Colors.green
                                  : Colors.red,
                            ),
                            const SizedBox(width: 8),
                            Text(
                              _result == 'true' ? '拨号成功' : '拨号失败',
                              style: TextStyle(
                                color: _result == 'true'
                                    ? Colors.green
                                    : Colors.red,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ],
                        ),
                      ),
                    const SizedBox(height: 24),
                    _buildQuickCallButtons(),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _makeCall() async {
    final phoneNumber = _numberController.text.trim();
  
    if (phoneNumber.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请输入电话号码')),
      );
      return;
    }

    setState(() {
      _isCalling = true;
      _result = null;
    });

    try {
      final result = await FlutterPhoneDirectCaller.callNumber(phoneNumber);
      setState(() {
        _result = result.toString();
      });
  
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(result == true ? '拨号成功' : '拨号失败'),
          backgroundColor: result == true ? Colors.green : Colors.red,
        ),
      );
    } catch (e) {
      setState(() {
        _result = 'error';
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('拨号失败: $e'),
          backgroundColor: Colors.red,
        ),
      );
    } finally {
      setState(() {
        _isCalling = false;
      });
    }
  }

  Widget _buildQuickCallButtons() {
    final quickCalls = [
      {'name': '客服电话', 'number': '4008888888', 'icon': Icons.headset_mic},
      {'name': '紧急电话', 'number': '110', 'icon': Icons.phone_in_talk},
      {'name': '技术支持', 'number': '4001234567', 'icon': Icons.support_agent},
    ];

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '快速拨号',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 16),
        ...quickCalls.map((call) => ListTile(
          leading: Icon(call['icon'] as IconData, color: Colors.green),
          title: Text(call['name'] as String),
          subtitle: Text(call['number'] as String),
          trailing: const Icon(Icons.call, color: Colors.green),
          onTap: () async {
            setState(() {
              _numberController.text = call['number'] as String;
            });
            await _makeCall();
          },
        )),
      ],
    );
  }
}

七、API 参考

7.1 FlutterPhoneDirectCaller API

方法 说明 参数 返回值 OpenHarmony 支持
callNumber 发起电话呼叫 String Future <bool> ✅ yes

7.2 参数说明

callNumber

  • 参数:number(String)- 要拨打的电话号码
  • 返回值:Future<bool>
    • true:拨号成功
    • false:拨号失败

八、常见问题与解决方案

8.1 拨号失败

问题描述:调用 callNumber 返回 false。

可能原因

  1. 设备不支持通话功能(如模拟器)
  2. 电话号码格式不正确
  3. 用户取消了拨号

解决方案

dart 复制代码
final result = await FlutterPhoneDirectCaller.callNumber(phoneNumber);
if (!result) {
  // 提示用户检查设备是否支持通话功能
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('拨号失败,请检查设备是否支持通话功能')),
  );
}

九、总结

本文详细介绍了 Flutter for OpenHarmony 中 flutter_phone_direct_caller 电话拨号插件的使用方法,包括:

  1. 基础概念:flutter_phone_direct_caller 的特点、应用场景
  2. 平台适配:工作原理、支持的功能
  3. 项目配置:依赖添加
  4. 核心 API:拨打电话
  5. 实际应用:联系客服、紧急呼叫
  6. 完整示例:完整的电话拨号应用
  7. 最佳实践:号码验证、用户确认、错误处理

flutter_phone_direct_caller 是一个简单易用的电话拨号插件,非常适合需要拨打电话功能的应用。API 简洁,一行代码即可实现拨打电话功能。


十、参考资料

📌 提示:本文基于 flutter_phone_direct_caller@2.2.1 和 OpenHarmony 适配版本编写。

相关推荐
不爱吃糖的程序媛2 小时前
Flutter-OH 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— 文件格式路由:.doc 与 .docx 的分流策略
flutter·harmonyos
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— FlutterPlugin 与 AbilityAware 双接口实现
flutter·harmonyos
LawrenceLan2 小时前
31.Flutter 零基础入门(三十一):Stack 与 Positioned —— 悬浮、角标与覆盖布局
开发语言·前端·flutter·dart
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— openLink API 与浏览器启动策略
flutter
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:第三方库实战 cryptography_flutter 加密解密详解
flutter
lqj_本人2 小时前
Flutter三方库适配OpenHarmony【apple_product_name】构建设备信息展示页面
flutter
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— 深度链接(Deep Link)机制全解析
flutter
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— FlutterPlugin 接口实现与 MethodChannel 注册
flutter·harmonyos