基础入门 Flutter for OpenHarmony:video_thumbnail 视频缩略图详解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 video_thumbnail 视频缩略图插件的使用方法,带你全面掌握从视频中提取缩略图、设置缩略图参数等功能。


一、video_thumbnail 组件概述

在 Flutter for OpenHarmony 应用开发中,video_thumbnail 是一个非常实用的插件,用于从视频文件中提取缩略图。它支持本地视频和网络视频,可以自定义缩略图的尺寸、格式、质量和提取时间点。

📋 video_thumbnail 组件特点

特点 说明
跨平台支持 支持 Android、iOS、OpenHarmony
多种来源 支持本地视频文件和网络视频 URL
多种格式 支持 JPEG、PNG、WebP 输出格式
尺寸控制 支持设置最大宽高限制
时间点选择 支持指定提取缩略图的时间点
质量控制 支持设置输出图片质量
两种输出方式 支持返回文件路径或内存数据
HTTP 头支持 支持自定义 HTTP 请求头

💡 使用场景:视频列表预览、视频编辑应用、媒体库管理、视频分享预览等需要显示视频缩略图的场景。


二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目适配 Flutter 3.7.12-ohos-1.0.6,SDK 5.0.0(12),DevEco Studio 5.0.13.200,ROM 5.1.0.120 SP3。

2.2 支持的功能

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

功能 说明 OpenHarmony 支持
thumbnailData() 生成缩略图数据(内存) ✅ yes
thumbnailFile() 生成缩略图文件 ✅ yes
JPEG 格式 ImageFormat.JPEG ✅ yes
PNG 格式 ImageFormat.PNG ✅ yes
WebP 格式 ImageFormat.WEBP ✅ yes
自定义尺寸 maxHeight / maxWidth ✅ yes
自定义质量 quality (0-100) ✅ yes
时间点选择 timeMs ✅ yes
HTTP 头 headers ✅ yes
本地视频 本地文件路径 ✅ yes
网络视频 URL 地址 ✅ yes (API>=20)

⚠️ 注意:网络视频需要 SDK API 版本 >= 20 才支持。如果 API 版本低于 20,会抛出错误 "Below API20 and an online url, thumbnail retrieval is not supported."


三、项目配置与安装

3.1 添加依赖配置

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

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

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

  # 添加 video_thumbnail 依赖(OpenHarmony 适配版本)
  video_thumbnail_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git
      path: ohos

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的 fluttertpc_video_thumbnail 仓库
  • url:指定 AtomGit托管的仓库地址
  • path:指定 ohos 包的具体路径
  • 本插件直接使用 video_thumbnail_ohos 包,无需额外的 dev_dependency

⚠️ 重要 :对于 OpenHarmony 平台,直接使用 video_thumbnail_ohos 包即可,这是鸿蒙平台的原生实现。

3.2 下载依赖

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

bash 复制代码
flutter pub get

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

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

3.3 权限配置

在 OpenHarmony 平台上,使用 video_thumbnail 需要配置读取媒体文件的权限。

ohos/entry/src/main/module.json5:

json 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:read_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

ohos/entry/src/main/resources/base/element/string.json:

添加权限申请原因:

json 复制代码
{
  "string": [
    {
      "name": "read_media_reason",
      "value": "读取视频文件生成缩略图"
    }
  ]
}

3.4 权限等级说明

在 OpenHarmony 系统中,权限分为三个等级:

等级 说明
normal 普通权限,普通应用可直接申请
system_basic 系统基础权限,需要系统签名或特殊配置
system_core 系统核心权限,仅系统应用可使用

重要提示ohos.permission.READ_IMAGEVIDEO 属于 system_basic 级别权限,默认的应用权限等级是 normal。如果直接安装包含此权限的应用,会报错 9568289

3.5 如何使用 system_basic 权限

要使用 READ_IMAGEVIDEO 权限,需要修改应用的权限等级。请参考华为官方文档:安装 HAP 时提示 code 9568289

步骤 1:修改 Debug 签名模板

找到 SDK 目录下的签名模板文件:

复制代码
{SDK路径}/openharmony/toolchains/lib/UnsgnedDebugProfileTemplate.json

例如:d:\huawei\DevEco Studio\sdk\default\openharmony\toolchains\lib\UnsgnedDebugProfileTemplate.json

打开文件,修改以下内容:

1. 确保 APL 等级为 system_basic:

json 复制代码
"bundle-info": {
    ...
    "apl": "system_basic",
    ...
}

2. 在 acls 中添加允许的权限:

json 复制代码
"acls": {
    "allowed-acls": [
        "ohos.permission.READ_IMAGEVIDEO"
    ]
}

3. 在 permissions 中添加受限权限:

json 复制代码
"permissions": {
    "restricted-permissions": [
        "ohos.permission.READ_IMAGEVIDEO"
    ]
}

完整的修改后的文件示例:

json 复制代码
{
    "version-name": "2.0.0",
    "version-code": 2,
    "uuid": "fe686e1b-3770-4824-a938-961b140a7c98",
    "validity": {
        "not-before": 1610519532,
        "not-after": 1705127532
    },
    "type": "debug",
    "bundle-info": {
        "developer-id": "OpenHarmony",
        "development-certificate": "...",
        "bundle-name": "com.OpenHarmony.app.test",
        "apl": "system_basic",
        "app-feature": "hos_normal_app"
    },
    "acls": {
        "allowed-acls": [
            "ohos.permission.READ_IMAGEVIDEO"
        ]
    },
    "permissions": {
        "restricted-permissions": [
            "ohos.permission.READ_IMAGEVIDEO"
        ]
    },
    "debug-info": {
        "device-ids": [...],
        "device-id-type": "udid"
    },
    "issuer": "pki_internal"
}
步骤 2:修改 Release 签名模板(可选)

如果需要发布应用,同样修改 UnsgnedReleasedProfileTemplate.json 文件。

步骤 3:在 DevEco Studio 中重新签名
  1. 打开 DevEco Studio
  2. 点击 File > Project Structure > Project > Signing Configs
  3. 取消勾选 "Automatically generate signature"
  4. 重新勾选 "Automatically generate signature"
  5. 等待自动签名完成
  6. 点击 OK
步骤 4:重新运行应用

签名完成后,重新运行应用即可正常安装。

⚠️ 注意:修改签名模板后必须重新签名才能生效。如果仍然报错,请尝试清理项目后重新构建。


四、video_thumbnail 基础用法

4.1 导入库

在使用 video_thumbnail 之前,需要先导入库:

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

4.2 图片格式枚举

video_thumbnail 支持三种输出格式:

dart 复制代码
enum ImageFormat { JPEG, PNG, WEBP }
格式 说明 特点
JPEG 有损压缩格式 文件小,适合照片类图像
PNG 无损压缩格式 支持透明,质量高
WebP 现代压缩格式 压缩率高,质量好

4.3 生成缩略图数据 - thumbnailData()

thumbnailData() 方法直接返回图片的二进制数据,适合直接在内存中使用:

dart 复制代码
Future<Uint8List?> _generateThumbnailData(String videoPath) async {
  final bytes = await VideoThumbnailOhos.thumbnailData(
    video: videoPath,
    imageFormat: ImageFormat.JPEG,
    maxHeight: 256,
    maxWidth: 256,
    timeMs: 0,
    quality: 50,
  );
  return bytes;
}

参数说明:

参数 类型 必填 说明
video String 视频文件路径或 URL
headers Map<String, String>? HTTP 请求头(网络视频用)
imageFormat ImageFormat 输出图片格式,默认 PNG
maxHeight int 最大高度,建议设置具体值
maxWidth int 最大宽度,建议设置具体值
timeMs int 提取时间点(毫秒),默认 0
quality int 图片质量(0-100),PNG 忽略此参数

⚠️ 注意maxHeightmaxWidth 建议设置具体值(如 256),设为 0 可能导致 FetchFrameByTime failed 错误。

4.4 生成缩略图文件 - thumbnailFile()

thumbnailFile() 方法将缩略图保存为文件,返回文件路径:

dart 复制代码
Future<String?> _generateThumbnailFile(String videoPath, String savePath) async {
  final thumbnailPath = await VideoThumbnailOhos.thumbnailFile(
    video: videoPath,
    thumbnailPath: savePath,
    imageFormat: ImageFormat.JPEG,
    maxHeight: 256,
    maxWidth: 256,
    timeMs: 0,
    quality: 50,
  );
  return thumbnailPath;
}

参数说明:

参数 类型 必填 说明
video String 视频文件路径或 URL
headers Map<String, String>? HTTP 请求头(网络视频用)
thumbnailPath String? 缩略图保存路径,null 则保存到视频同目录
imageFormat ImageFormat 输出图片格式,默认 PNG
maxHeight int 最大高度,建议设置具体值
maxWidth int 最大宽度,建议设置具体值
timeMs int 提取时间点(毫秒),默认 0
quality int 图片质量(0-100),PNG 忽略此参数

4.5 显示缩略图

生成缩略图数据后,可以直接使用 Image.memory() 显示:

dart 复制代码
Widget _buildThumbnail(Uint8List? thumbnailData) {
  if (thumbnailData == null) {
    return const Icon(Icons.video_library, size: 100);
  }
  return Image.memory(
    thumbnailData,
    fit: BoxFit.cover,
  );
}

五、完整示例:视频缩略图生成器

下面是一个完整的示例,展示如何实现视频缩略图生成功能:

dart 复制代码
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Thumbnail Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String? _videoPath;
  Uint8List? _thumbnailData;
  String? _thumbnailPath;
  bool _isLoading = false;
  String? _error;

  ImageFormat _format = ImageFormat.JPEG;
  int _quality = 50;
  int _maxHeight = 256;
  int _maxWidth = 256;
  int _timeMs = 0;

  Future<void> _pickVideo() async {
    final picker = ImagePicker();
    final video = await picker.pickVideo(source: ImageSource.gallery);

    if (video != null) {
      setState(() {
        _videoPath = video.path;
        _thumbnailData = null;
        _thumbnailPath = null;
        _error = null;
      });
    }
  }

  Future<void> _generateThumbnailData() async {
    if (_videoPath == null) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final bytes = await VideoThumbnailOhos.thumbnailData(
        video: _videoPath!,
        imageFormat: _format,
        maxHeight: _maxHeight,
        maxWidth: _maxWidth,
        timeMs: _timeMs,
        quality: _quality,
      );

      setState(() {
        _thumbnailData = bytes;
        _thumbnailPath = null;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  Future<void> _generateThumbnailFile() async {
    if (_videoPath == null) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final tempDir = await getTemporaryDirectory();
      final path = await VideoThumbnailOhos.thumbnailFile(
        video: _videoPath!,
        thumbnailPath: tempDir.path,
        imageFormat: _format,
        maxHeight: _maxHeight,
        maxWidth: _maxWidth,
        timeMs: _timeMs,
        quality: _quality,
      );

      setState(() {
        _thumbnailPath = path;
        _thumbnailData = null;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Video Thumbnail Demo'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 视频选择区域
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('视频文件', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    if (_videoPath != null)
                      Text(_videoPath!, style: const TextStyle(fontSize: 12)),
                    const SizedBox(height: 8),
                    ElevatedButton.icon(
                      icon: const Icon(Icons.video_library),
                      label: const Text('选择视频'),
                      onPressed: _pickVideo,
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // 参数设置区域
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('缩略图参数', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                  
                    // 格式选择
                    Row(
                      children: [
                        const Text('格式: '),
                        DropdownButton<ImageFormat>(
                          value: _format,
                          items: const [
                            DropdownMenuItem(value: ImageFormat.JPEG, child: Text('JPEG')),
                            DropdownMenuItem(value: ImageFormat.PNG, child: Text('PNG')),
                            DropdownMenuItem(value: ImageFormat.WEBP, child: Text('WebP')),
                          ],
                          onChanged: (v) => setState(() => _format = v!),
                        ),
                      ],
                    ),
                  
                    // 质量滑块
                    Row(
                      children: [
                        const Text('质量: '),
                        Expanded(
                          child: Slider(
                            value: _quality.toDouble(),
                            min: 0,
                            max: 100,
                            divisions: 100,
                            label: '$_quality',
                            onChanged: (v) => setState(() => _quality = v.toInt()),
                          ),
                        ),
                        Text('$_quality'),
                      ],
                    ),
                  
                    // 时间点滑块
                    Row(
                      children: [
                        const Text('时间: '),
                        Expanded(
                          child: Slider(
                            value: _timeMs.toDouble(),
                            min: 0,
                            max: 10000,
                            divisions: 100,
                            label: '${_timeMs}ms',
                            onChanged: (v) => setState(() => _timeMs = v.toInt()),
                          ),
                        ),
                        Text('$_timeMs ms'),
                      ],
                    ),
                  
                    // 尺寸设置
                    Row(
                      children: [
                        const Text('尺寸: '),
                        Expanded(
                          child: Slider(
                            value: _maxWidth.toDouble(),
                            min: 0,
                            max: 500,
                            divisions: 50,
                            label: '$_maxWidth',
                            onChanged: (v) => setState(() {
                              _maxWidth = v.toInt();
                              _maxHeight = v.toInt();
                            }),
                          ),
                        ),
                        Text('$_maxWidth x $_maxHeight'),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // 操作按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton.icon(
                  icon: const Icon(Icons.memory),
                  label: const Text('生成数据'),
                  onPressed: _videoPath != null && !_isLoading ? _generateThumbnailData : null,
                ),
                ElevatedButton.icon(
                  icon: const Icon(Icons.save),
                  label: const Text('生成文件'),
                  onPressed: _videoPath != null && !_isLoading ? _generateThumbnailFile : null,
                ),
              ],
            ),
            const SizedBox(height: 16),

            // 结果显示区域
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('缩略图预览', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    if (_isLoading)
                      const Center(child: CircularProgressIndicator())
                    else if (_error != null)
                      Text('错误: $_error', style: const TextStyle(color: Colors.red))
                    else if (_thumbnailData != null)
                      Column(
                        children: [
                          Image.memory(_thumbnailData!, fit: BoxFit.cover),
                          const SizedBox(height: 8),
                          Text('数据大小: ${_thumbnailData!.length} bytes'),
                        ],
                      )
                    else if (_thumbnailPath != null)
                      Column(
                        children: [
                          Image.file(File(_thumbnailPath!), fit: BoxFit.cover),
                          const SizedBox(height: 8),
                          Text('文件路径: $_thumbnailPath'),
                        ],
                      )
                    else
                      const Center(
                        child: Icon(Icons.image, size: 100, color: Colors.grey),
                      ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

六、高级用法

6.1 带进度指示的缩略图生成

使用 FutureBuilder 实现带进度指示的缩略图生成:

dart 复制代码
class ThumbnailGenerator extends StatelessWidget {
  final String videoPath;
  final int width;
  final int height;
  final int timeMs;

  const ThumbnailGenerator({
    Key? key,
    required this.videoPath,
    this.width = 200,
    this.height = 200,
    this.timeMs = 0,
  }) : super(key: key);

  Future<Uint8List?> _generateThumbnail() async {
    return await VideoThumbnailOhos.thumbnailData(
      video: videoPath,
      imageFormat: ImageFormat.JPEG,
      maxHeight: height,
      maxWidth: width,
      timeMs: timeMs,
      quality: 75,
    );
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List?>(
      future: _generateThumbnail(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasData && snapshot.data != null) {
            return Image.memory(snapshot.data!, fit: BoxFit.cover);
          } else if (snapshot.hasError) {
            return const Icon(Icons.error, color: Colors.red);
          }
          return const Icon(Icons.video_library);
        }
        return const CircularProgressIndicator();
      },
    );
  }
}

6.2 批量生成缩略图

为视频列表批量生成缩略图:

dart 复制代码
Future<Map<String, Uint8List>> _generateBatchThumbnails(List<String> videoPaths) async {
  final Map<String, Uint8List> results = {};
  
  for (final path in videoPaths) {
    try {
      final bytes = await VideoThumbnailOhos.thumbnailData(
        video: path,
        imageFormat: ImageFormat.JPEG,
        maxHeight: 120,
        maxWidth: 160,
        quality: 60,
      );
      if (bytes != null) {
        results[path] = bytes;
      }
    } catch (e) {
      debugPrint('Failed to generate thumbnail for $path: $e');
    }
  }
  
  return results;
}

6.3 网络视频缩略图

⚠️ 注意:OpenHarmony 平台目前对网络视频生成缩略图存在已知问题,建议先将视频下载到本地再生成缩略图。

dart 复制代码
Future<Uint8List?> _generateNetworkThumbnail(String url) async {
  // 建议先下载视频到本地
  // 然后使用本地路径生成缩略图
  
  final bytes = await VideoThumbnailOhos.thumbnailData(
    video: url,
    headers: {
      'User-Agent': 'Mozilla/5.0',
      'Authorization': 'Bearer token',
    },
    imageFormat: ImageFormat.JPEG,
    maxHeight: 256,
    maxWidth: 256,
    quality: 50,
  );
  return bytes;
}

七、常见问题与解决方案

7.1 缩略图生成失败

问题:生成缩略图时返回 null 或抛出异常。

解决方案

  1. 检查视频文件路径是否正确
  2. 确保已配置 READ_IMAGEVIDEO 权限
  3. 检查视频格式是否支持
  4. 对于网络视频,建议先下载到本地

7.2 缩略图质量不佳

问题:生成的缩略图模糊或质量差。

解决方案

  • 提高输出尺寸(maxHeight/maxWidth)
  • 提高质量参数(quality)
  • 使用 PNG 格式(无损压缩)
dart 复制代码
final bytes = await VideoThumbnailOhos.thumbnailData(
  video: videoPath,
  imageFormat: ImageFormat.PNG,
  maxHeight: 400,
  maxWidth: 400,
  quality: 100,
);

7.3 内存占用过高

问题:批量生成缩略图时内存占用过高。

解决方案

  • 限制缩略图尺寸
  • 及时释放不需要的缩略图数据
  • 使用文件方式而非内存方式
dart 复制代码
// 使用文件方式减少内存占用
final path = await VideoThumbnailOhos.thumbnailFile(
  video: videoPath,
  thumbnailPath: tempDir,
  maxHeight: 120,
  maxWidth: 160,
  quality: 60,
);

八、最佳实践

8.1 缩略图尺寸建议

使用场景 建议尺寸 建议格式 建议质量
列表预览 120 x 160 JPEG 60
详情页预览 200 x 300 JPEG 75
全屏预览 400 x 600 JPEG/PNG 85
高清预览 原始尺寸 PNG 100

8.2 性能优化建议

  • 对于列表显示,使用较小的缩略图尺寸
  • 批量生成时考虑使用队列避免并发过高
  • 缓存已生成的缩略图避免重复生成
  • 使用文件方式存储缩略图便于后续使用

8.3 错误处理建议

dart 复制代码
Future<Uint8List?> safeGenerateThumbnail(String videoPath) async {
  try {
    if (!File(videoPath).existsSync()) {
      debugPrint('Video file not found: $videoPath');
      return null;
    }
  
    final bytes = await VideoThumbnailOhos.thumbnailData(
      video: videoPath,
      imageFormat: ImageFormat.JPEG,
      maxHeight: 256,
      maxWidth: 256,
      quality: 50,
    );
  
    return bytes;
  } on PlatformException catch (e) {
    debugPrint('Platform error: ${e.message}');
    return null;
  } catch (e) {
    debugPrint('Unknown error: $e');
    return null;
  }
}

九、总结

本文详细介绍了 Flutter for OpenHarmony 中 video_thumbnail 插件的使用方法,包括:

  • ✅ 插件的基本概念和特点
  • ✅ OpenHarmony 平台的适配说明
  • ✅ 依赖配置和权限设置
  • ✅ 基础用法和 API 详解
  • ✅ 完整的示例代码
  • ✅ 高级用法技巧
  • ✅ 常见问题与解决方案
  • ✅ 最佳实践建议

通过本文的学习,你应该能够在 Flutter for OpenHarmony 项目中熟练使用 video_thumbnail 插件实现视频缩略图生成功能。


📌 参考资源

相关推荐
lqj_本人2 小时前
Flutter三方库适配OpenHarmony【apple_product_name】华为Pura系列设备映射表
flutter·华为
空白诗3 小时前
基础入门 Flutter for OpenHarmony:Divider 分割线组件详解
flutter
阿林来了5 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别停止与取消
flutter·语音识别·harmonyos
哈__5 小时前
基础入门 Flutter for OpenHarmony:flutter_slidable 列表滑动操作详解
flutter
哈__6 小时前
基础入门 Flutter for OpenHarmony:mobile_device_identifier 设备唯一标识详解
flutter
松叶似针6 小时前
Flutter三方库适配OpenHarmony【secure_application】— 应用生命周期回调注册
flutter·harmonyos
哈__7 小时前
基础入门 Flutter for OpenHarmony:battery_plus 电池状态监控详解
flutter
键盘鼓手苏苏7 小时前
Flutter for OpenHarmony 实战:flutter_redux 全局状态机与单向数据流
flutter·华为·harmonyos
阿林来了8 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 麦克风权限申请实现
flutter·harmonyos