基础入门 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 选择文件
相关推荐
Lanren的编程日记10 小时前
Flutter鸿蒙应用开发:生物识别(指纹/面容)功能集成实战
flutter·华为·harmonyos
Lanren的编程日记13 小时前
Flutter鸿蒙应用开发:基础UI组件库设计与实现实战
flutter·ui·harmonyos
西西学代码14 小时前
Flutter---波形动画
flutter
于慨17 小时前
flutter基础组件用法
开发语言·javascript·flutter
恋猫de小郭19 小时前
Android CLI ,谷歌为 Android 开发者专研的 AI Agent,提速三倍
android·前端·flutter
火柴就是我20 小时前
flutter pushAndRemoveUntil 的一次小疑惑
flutter
于慨20 小时前
flutter doctor问题解决
flutter
唔6621 小时前
flutter 图片加载类 图片的安全使用
安全·flutter
Nathan202406161 天前
Flutter - InheritedWidget
flutter·dart
恋猫de小郭1 天前
JetBrains Amper 0.10 ,期待它未来替代 Gradle
android·前端·flutter