基础入门 Flutter for OpenHarmony:url_launcher 链接启动详解

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


一、url_launcher 组件概述

在 Flutter for OpenHarmony 应用开发中,url_launcher 是一个非常实用的插件,用于在应用内打开外部链接、启动其他应用、拨打电话、发送邮件等。它提供了一个统一的 API,屏蔽了不同平台的差异,让开发者可以轻松实现各种跳转功能。

📋 url_launcher 组件特点

特点 说明
跨平台支持 支持 Android、iOS、Linux、macOS、Windows、OpenHarmony
统一 API 不同平台使用相同的 API 进行跳转
多种启动模式 支持浏览器、应用内 WebView、外部应用等模式
功能丰富 支持打开链接、拨打电话、发送邮件等
异步操作 所有跳转操作都是异步的

💡 使用场景:打开网页、拨打电话、发送邮件、打开应用商店、启动其他应用等。


二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目基于 url_launcher@6.3.3 开发,适配 Flutter 3.27.5-ohos-1.0.4。

2.2 支持的功能

在 OpenHarmony 平台上,url_launcher 支持以下功能:

功能 说明 OpenHarmony 支持
canLaunch() 检查是否可以启动指定 URL ✅ yes
launch() 启动指定 URL(旧 API) ✅ yes
launchUrl() 启动指定 URL(新 API) ✅ yes
closeWebView() 关闭 WebView ✅ yes

三、项目配置与安装

3.1 添加依赖配置

首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 url_launcher 依赖。

打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:

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

  # 添加 url_launcher 依赖(OpenHarmony 适配版本)
  url_launcher:
    git:
      url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
      path: "packages/url_launcher/url_launcher"

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的 flutter_packages 仓库
  • url:指定 GitCode 托管的仓库地址
  • path:指定 url_launcher 包的具体路径
  • 本项目基于 url_launcher@6.3.3 开发,适配 Flutter 3.27.5-ohos-1.0.4

⚠️ 重要:对于 OpenHarmony 平台,必须使用 git 方式引用适配版本,不能直接使用 pub.dev 的版本号。

3.2 下载依赖

配置完成后,需要在项目根目录执行以下命令下载依赖:

bash 复制代码
flutter pub get

执行成功后,你会看到类似以下的输出:

复制代码
Running "flutter pub get" in my_cross_platform_app...
Resolving dependencies...
Got dependencies!

3.3 权限配置说明

url_launcher 在 OpenHarmony 平台上需要配置网络权限(ohos.permission.INTERNET),否则在安装 hap 包时可能会报错 9568289

3.3.1 配置应用权限等级

默认的应用权限等级是 normal,但某些权限需要 system_basic 等级。如果遇到权限问题,需要修改应用等级。

修改应用等级步骤:

  1. 打开 ohos/AppScope/app.json5 文件
  2. 找到 app 对象,修改 bundleType 或其他相关配置
  3. 具体配置请参考华为官方文档:安装hap时提示code9568289
3.3.2 在 module.json5 中添加权限

打开 ohos/entry/src/main/module.json5 文件,在 requestPermissions 数组中添加网络权限:

json5 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "default",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:network_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}

配置说明:

  • name:权限名称,这里是 ohos.permission.INTERNET
  • reason:申请权限的原因,引用字符串资源
  • usedScene:使用场景,指定在哪些 Ability 中使用,以及使用时机
3.3.3 添加权限原因说明

打开 ohos/entry/src/main/resources/base/element/string.json 文件,添加权限原因的字符串:

json 复制代码
{
  "string": [
    {
      "name": "network_reason",
      "value": "使用网络"
    }
  ]
}

注意: 如果你的 string.json 文件已经有其他内容,只需在 string 数组中添加新的对象即可,不要覆盖原有内容。

3.4 依赖自动配置说明

执行 flutter pub get 后,OpenHarmony 平台的依赖会自动配置到 ohos/entry/oh-package.json5 文件中。你不需要手动配置 OpenHarmony 端的依赖,Flutter 构建系统会自动处理。


四、url_launcher 基础用法

4.1 导入包

在使用 url_launcher 之前,首先需要导入相关包:

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

4.2 检查是否可以启动 URL - canLaunch()

在尝试启动 URL 之前,建议先检查设备是否支持该 URL:

dart 复制代码
// 检查是否可以启动 URL
bool canLaunchUrl = await canLaunch('https://www.example.com');

if (canLaunchUrl) {
  // 可以启动
  await launchUrl(Uri.parse('https://www.example.com'));
} else {
  // 不支持
  print('无法启动此 URL');
}

4.3 启动 URL(新 API)- launchUrl()

使用新的 launchUrl() 方法启动 URL,推荐使用此方法:

dart 复制代码
// 在默认浏览器中打开 URL
final Uri url = Uri.parse('https://www.example.com');
await launchUrl(url);

// 在应用内 WebView 中打开 URL
await launchUrl(
  url,
  mode: LaunchMode.inAppWebView,
);

// 在外部应用中打开 URL
await launchUrl(
  url,
  mode: LaunchMode.externalApplication,
);

4.4 启动 URL(旧 API)- launch()

旧版本的 launch() 方法仍然可用,但推荐使用新的 launchUrl()

dart 复制代码
// 在默认浏览器中打开 URL
await launch('https://www.example.com');

// 在应用内 WebView 中打开 URL
await launch(
  'https://www.example.com',
  useSafariVC: true,
  useWebView: true,
);

// 拨打电话
await launch('tel:1234567890');

// 发送邮件
await launch('mailto:example@example.com');

4.5 关闭 WebView - closeWebView()

当使用 inAppWebView 模式打开 URL 后,可以通过以下方式关闭 WebView:

dart 复制代码
// 打开 URL
await launchUrl(
  url,
  mode: LaunchMode.inAppWebView,
);

// 5 秒后关闭
Future.delayed(const Duration(seconds: 5), () {
  // 注意:closeWebView() 在某些平台上可能不可用
  // 在 OpenHarmony 上,可以通过 headers 配置页面路径
});

五、常见使用场景

5.1 打开网页

dart 复制代码
// 打开网页(使用默认浏览器)
final Uri url = Uri.parse('https://www.openharmony.cn');
if (await canLaunchUrl(url)) {
  await launchUrl(url);
} else {
  throw Exception('无法打开链接');
}

5.2 拨打电话

dart 复制代码
// 拨打电话
final Uri phoneUri = Uri(scheme: 'tel', path: '1234567890');
if (await canLaunchUrl(phoneUri)) {
  await launchUrl(phoneUri);
} else {
  print('不支持拨打电话');
}

5.3 发送邮件

dart 复制代码
// 发送邮件
final Uri emailUri = Uri(
  scheme: 'mailto',
  path: 'example@example.com',
  query: 'subject=主题&body=邮件内容',
);
if (await canLaunchUrl(emailUri)) {
  await launchUrl(emailUri);
}

5.4 打开应用商店

dart 复制代码
// 打开华为应用商店
final Uri appStoreUri = Uri.parse(
  'store://appgallery.huawei.com/app/detail?id=com.huawei.hmsapp.himovie'
);
if (await canLaunchUrl(appStoreUri)) {
  await launchUrl(appStoreUri);
}

5.5 在应用内 WebView 中打开网页

dart 复制代码
// 在应用内 WebView 中打开网页
await launchUrl(
  Uri.parse('https://www.example.com'),
  mode: LaunchMode.inAppWebView,
  webViewConfiguration: const WebViewConfiguration(
    enableJavaScript: true,
    enableDomStorage: true,
  ),
);

六、完整示例代码

下面是一个完整的示例应用,展示了 url_launcher 的各种用法:

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

void main() {
  runApp(const UrlLauncherDemo());
}

class UrlLauncherDemo extends StatelessWidget {
  const UrlLauncherDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'URL Launcher 示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const UrlLauncherPage(),
    );
  }
}

class UrlLauncherPage extends StatefulWidget {
  const UrlLauncherPage({super.key});

  @override
  State<UrlLauncherPage> createState() => _UrlLauncherPageState();
}

class _UrlLauncherPageState extends State<UrlLauncherPage> {
  bool _hasCallSupport = false;
  Future<void>? _launched;
  String _phone = '';

  @override
  void initState() {
    super.initState();
    _checkCallSupport();
  }

  Future<void> _checkCallSupport() async {
    bool canCall = await canLaunchUrl(Uri.parse('tel:123'));
    setState(() {
      _hasCallSupport = canCall;
    });
  }

  Future<void> _launchInBrowser(String url) async {
    final Uri uri = Uri.parse(url);
    if (!await launchUrl(uri)) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('无法打开链接: $url')),
        );
      }
    }
  }

  Future<void> _launchInWebView(String url) async {
    final Uri uri = Uri.parse(url);
    if (!await launchUrl(
      uri,
      mode: LaunchMode.inAppWebView,
    )) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('无法在 WebView 中打开: $url')),
        );
      }
    }
  }

  Future<void> _launchEmail() async {
    final Uri emailUri = Uri(
      scheme: 'mailto',
      path: 'example@example.com',
      query: 'subject=主题&body=邮件内容',
    );
    if (!await launchUrl(emailUri)) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('无法打开邮件应用')),
        );
      }
    }
  }

Future<void> _makePhoneCall() async {
  // 检查电话号码是否为空
  if (_phone.isEmpty) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请输入电话号码')),
      );
    }
    return;
  }

  try {
    final Uri phoneUri = Uri(
      scheme: 'tel',
      path: _phone.trim(),
    );
  
    // 先检查是否可以启动
    if (!await canLaunchUrl(phoneUri)) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('无法拨打电话')),
        );
      }
      return;
    }

    // 启动拨号
    await launchUrl(phoneUri);
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('拨打电话失败: $e')),
      );
    }
  }
}


  Future<void> _launchAppGallery() async {
    final Uri appGalleryUri = Uri.parse(
      'store://appgallery.huawei.com/app/detail?id=com.huawei.hmsapp.himovie'
    );
    if (!await launchUrl(appGalleryUri)) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('无法打开应用商店')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    const String testUrl = 'https://www.openharmony.cn';

    return Scaffold(
      appBar: AppBar(
        title: const Text('URL Launcher 示例'),
      ),
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [
              Colors.blue.shade50,
              Colors.indigo.shade50,
            ],
          ),
        ),
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildSectionCard(
              title: '打开网页',
              icon: Icons.language,
              color: Colors.blue,
              child: Column(
                children: [
                  const Text(
                    testUrl,
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey,
                    ),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: () => _launchInBrowser(testUrl),
                          icon: const Icon(Icons.open_in_browser),
                          label: const Text('在浏览器中打开'),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: OutlinedButton.icon(
                          onPressed: () => _launchInWebView(testUrl),
                          icon: const Icon(Icons.web),
                          label: const Text('在 WebView 中打开'),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),

            const SizedBox(height: 16),

            _buildSectionCard(
              title: '拨打电话',
              icon: Icons.phone,
              color: Colors.green,
              child: Column(
                children: [
                  TextField(
                    onChanged: (value) => _phone = value,
                    decoration: const InputDecoration(
                      labelText: '输入电话号码',
                      prefixIcon: Icon(Icons.phone),
                      border: OutlineInputBorder(),
                    ),
                    keyboardType: TextInputType.phone,
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _hasCallSupport ? _makePhoneCall : null,
                    icon: const Icon(Icons.call),
                    label: Text(_hasCallSupport ? '拨打电话' : '不支持拨打电话'),
                  ),
                ],
              ),
            ),

            const SizedBox(height: 16),

            _buildSectionCard(
              title: '发送邮件',
              icon: Icons.email,
              color: Colors.orange,
              child: ElevatedButton.icon(
                onPressed: _launchEmail,
                icon: const Icon(Icons.send),
                label: const Text('发送邮件'),
              ),
            ),

            const SizedBox(height: 16),

            _buildSectionCard(
              title: '打开应用商店',
              icon: Icons.store,
              color: Colors.purple,
              child: ElevatedButton.icon(
                onPressed: _launchAppGallery,
                icon: const Icon(Icons.apps),
                label: const Text('打开华为应用商店'),
              ),
            ),

            const SizedBox(height: 32),
          ],
        ),
      ),
    );
  }

  Widget _buildSectionCard({
    required String title,
    required IconData icon,
    required Color color,
    required Widget child,
  }) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Container(
                  padding: const EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: color.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Icon(
                    icon,
                    color: color,
                    size: 24,
                  ),
                ),
                const SizedBox(width: 12),
                Text(
                  title,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            child,
          ],
        ),
      ),
    );
  }
}

七、API 参数详解

7.1 LaunchMode(启动模式)

模式 说明 OpenHarmony 支持
LaunchMode.platformDefault 由平台决定启动方式 ✅ yes
LaunchMode.inAppWebView 在应用内 WebView 中打开 ✅ yes
LaunchMode.externalApplication 在外部应用中打开 ✅ yes
LaunchMode.externalNonBrowserApplication 在非浏览器外部应用中打开 ✅ yes

7.2 WebViewConfiguration(WebView 配置)

参数 说明 类型 OpenHarmony 支持
enableJavaScript 是否启用 JavaScript bool ✅ yes
enableDomStorage 是否启用 DOM 存储 bool ✅ yes
headers 请求头 Map<String, String> ✅ yes

7.3 LaunchOptions(启动选项)

参数 说明 类型 OpenHarmony 支持
mode 启动模式 LaunchMode ✅ yes
webViewConfiguration WebView 配置 WebViewConfiguration ✅ yes
webOnlyWindowName 窗口名称(仅 Web) String? ✅ yes

八、常见问题与最佳实践

8.1 常见问题

Q1: 为什么打开链接失败?

A: 检查以下几点:

  • 确认 URL 格式正确(使用 Uri.parse() 解析)
  • 使用 canLaunchUrl() 检查是否支持该 URL
  • 确认已正确配置网络权限
Q2: 如何在 OpenHarmony 上使用应用内 WebView?

A: 使用 LaunchMode.inAppWebView 模式:

dart 复制代码
await launchUrl(
  url,
  mode: LaunchMode.inAppWebView,
);
Q3: 如何传递自定义请求头?

A: 使用 WebViewConfigurationheaders 参数:

dart 复制代码
await launchUrl(
  url,
  mode: LaunchMode.inAppWebView,
  webViewConfiguration: WebViewConfiguration(
    headers: {
      'Custom-Header': 'value',
    },
  ),
);

8.2 最佳实践

1. 始终使用 Uri.parse()
dart 复制代码
// ❌ 错误示例
await launchUrl('https://www.example.com');

// ✅ 正确示例
await launchUrl(Uri.parse('https://www.example.com'));
2. 使用 canLaunchUrl() 检查
dart 复制代码
// 检查是否可以启动
final Uri url = Uri.parse('https://www.example.com');
if (await canLaunchUrl(url)) {
  await launchUrl(url);
} else {
  // 处理不支持的情况
}
3. 选择合适的启动模式
dart 复制代码
// 需要用户留在应用内 - 使用 inAppWebView
await launchUrl(url, mode: LaunchMode.inAppWebView);

// 需要打开外部应用 - 使用 externalApplication
await launchUrl(url, mode: LaunchMode.externalApplication);
4. 处理异常
dart 复制代码
try {
  await launchUrl(url);
} catch (e) {
  print('启动失败: $e');
  // 提示用户
}

九、总结

恭喜你!通过这篇文章的学习,你已经掌握了 Flutter 中 url_launcher 链接启动的全面知识。

🎯 核心要点

  1. 基础用法 :使用 launchUrl() 启动各种 URL
  2. 多种模式:支持浏览器、WebView、外部应用等启动模式
  3. 异步操作 :所有跳转操作都是异步的,必须使用 await 等待
  4. 错误处理 :使用 canLaunchUrl() 检查是否支持,处理异常情况
  5. 权限配置:OpenHarmony 需要配置网络权限

📚 使用场景指南

场景 推荐方法 启动模式
打开网页 launchUrl() platformDefault 或 inAppWebView
拨打电话 launchUrl(Uri(scheme: 'tel')) externalApplication
发送邮件 launchUrl(Uri(scheme: 'mailto')) externalApplication
打开应用商店 launchUrl() externalApplication
应用内浏览 launchUrl() inAppWebView

🚀 进阶方向

掌握了 url_launcher 后,你还可以探索以下方向:

  1. 深链接:学习使用 uni_links 处理应用深链接
  2. 应用跳转:学习如何从其他应用跳转到你的应用
  3. WebView 高级:学习使用 webview_flutter 实现更复杂的网页展示
  4. 文件处理:学习使用 file_selector 选择文件
相关推荐
哈__2 小时前
基础入门 Flutter for OpenHarmony:shared_preferences 轻量级存储详解
flutter
早點睡3902 小时前
基础入门 Flutter for OpenHarmony:DropdownButton 下拉按钮组件详解
flutter·harmonyos
加农炮手Jinx3 小时前
Flutter for OpenHarmony 实战:network_info_plus 网络扫描与隐私合规深度适配
网络·flutter·华为·harmonyos·鸿蒙
早點睡3903 小时前
基础入门 Flutter for OpenHarmony:AlertDialog 对话框组件详解
flutter·harmonyos
哈__3 小时前
基础入门 Flutter for OpenHarmony:image_picker 图片选择详解
flutter
早點睡3904 小时前
基础入门 Flutter for OpenHarmony:FloatingActionButton 浮动按钮详解
flutter·harmonyos
哈__4 小时前
基础入门 Flutter for OpenHarmony:file_selector 文件选择详解
flutter
左手厨刀右手茼蒿4 小时前
Flutter for OpenHarmony 实战:DartX — 极致简练的开发超能力集
android·flutter·ui·华为·harmonyos
空白诗4 小时前
基础入门 Flutter for OpenHarmony:TabBar 标签栏组件详解
flutter·harmonyos