基础入门 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 适配版本编写。

相关推荐
风华圆舞1 天前
在 Flutter 鸿蒙项目里接入语音识别的完整思路
flutter·语音识别·harmonyos
风华圆舞1 天前
鸿蒙 + Flutter 下如何让 HarmonyOS 能力真正服务于 AI 体验
人工智能·flutter·harmonyos
BreezeDove1 天前
【Android】Flutter3.35项目启动超时问题
android·flutter
风华圆舞1 天前
鸿蒙 MICROPHONE 权限在 Flutter 项目里怎么处理
flutter·华为·harmonyos
愚者Pro2 天前
切换本地 Flutter SDK 版本
flutter
TT_Close2 天前
别再复制旧 Flutter 工程了,真正拖慢你的不是业务代码
flutter·npm·visual studio code
风华圆舞2 天前
鸿蒙 + Flutter 下 AI 助手为什么要支持流式输出
人工智能·flutter·harmonyos
风华圆舞2 天前
鸿蒙 + Flutter 下 AI 页面的状态协同设计
人工智能·flutter·harmonyos
风华圆舞2 天前
鸿蒙语音播报功能 的 Flutter 侧封装思路
flutter·华为·harmonyos
brycegao3212 天前
Flutter 国际化富文本解决方案:基于双层占位符的轻量化图文混排方案
flutter·国际化·i18n·富文本·rtl·移动端工程架构