Flutter_OpenHarmony_三方库_image_picker图片视频采集适配详解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


一、image_picker 库简介

image_picker 是 Flutter 官方维护的插件库之一,用于从设备相册或相机获取图片和视频。无论是用户头像上传、图片分享、视频录制,还是多媒体内容管理,image_picker 都是移动应用开发中不可或缺的工具。

📋 image_picker 核心特点

特点 说明
图片选择 支持从相册选择单张或多张图片
视频选择 支持从相册选择视频或录制视频
相机拍摄 支持直接调用相机拍照或录像
图片压缩 支持设置最大宽高和图片质量
多选支持 支持一次性选择多张图片
媒体混合 支持同时选择图片和视频
跨平台兼容 支持 Android、iOS、Web、OpenHarmony

平台功能支持对比

功能 Android iOS Web OpenHarmony
从相册选图 ✔️ ✔️ ✔️ ✔️
从相机拍照 ✔️ ✔️ ✔️
从相册选视频 ✔️ ✔️ ✔️ ✔️
从相机录像 ✔️ ✔️ ✔️
多选图片 ✔️ ✔️ (iOS 14+) ✔️ ✔️
多选媒体 ✔️ ✔️ (iOS 14+) ✔️ ✔️
图片压缩 ✔️ ✔️ ✔️
获取元数据 ✔️ ✔️ ✔️

使用场景

  • 用户头像上传
  • 图片/视频分享应用
  • 社交媒体内容发布
  • 文档扫描与识别
  • 多媒体文件管理

二、OpenHarmony 适配版本

2.1 环境说明

组件 版本
Flutter 3.27.5
HarmonyOS 6.0
image_picker 1.1.2 (OpenHarmony 适配版本)

2.2 引入方式

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

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

  # image_picker OpenHarmony 适配版本
  image_picker:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages
      path: packages/image_picker/image_picker

2.3 获取依赖

配置完成后,在项目根目录执行:

bash 复制代码
flutter pub get

2.4 权限配置

image_picker 在 OpenHarmony 平台需要配置相机和媒体权限。

打开 ohos/entry/src/main/module.json5,在 requestPermissions 中添加:

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

ohos/entry/src/main/resources/base/element/string.json 中添加权限说明:

json 复制代码
{
  "name": "camera_reason",
  "value": "使用相机拍摄照片和视频"
},
{
  "name": "media_reason",
  "value": "访问相册中的图片和视频"
}

三、核心 API 讲解

3.1 ImagePicker 类

ImagePicker 是插件的核心类,提供了所有图片和视频选择的方法。

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

final ImagePicker picker = ImagePicker();

3.2 枚举类型

ImageSource(图片来源)
dart 复制代码
enum ImageSource {
  camera,  // 从相机拍摄
  gallery, // 从相册选择
}
CameraDevice(相机设备)
dart 复制代码
enum CameraDevice {
  rear,  // 后置相机
  front, // 前置相机
}

3.3 主要 API 详解

3.3.1 pickImage - 选择单张图片
dart 复制代码
Future<XFile?> pickImage({
  required ImageSource source,           // 必填:图片来源(camera 或 gallery)
  double? maxWidth,                      // 可选:图片最大宽度
  double? maxHeight,                     // 可选:图片最大高度
  int? imageQuality,                     // 可选:图片质量 (0-100)
  CameraDevice preferredCameraDevice = CameraDevice.rear, // 可选:优先使用的相机
  bool requestFullMetadata = true,       // 可选:是否请求完整元数据
})

参数说明:

参数 类型 必填 默认值 说明
source ImageSource - 图片来源:camera(相机)或 gallery(相册)
maxWidth double? null 图片最大宽度,超出会等比缩放
maxHeight double? null 图片最大高度,超出会等比缩放
imageQuality int? null 图片质量,范围 0-100,100 为原图质量
preferredCameraDevice CameraDevice CameraDevice.rear 优先使用的相机:rear(后置)或 front(前置)
requestFullMetadata bool true 是否获取完整元数据(EXIF 信息等)

返回值: Future<XFile?> - 返回选中的图片文件对象,用户取消时返回 null

使用示例:

dart 复制代码
// 从相册选择图片
final XFile? image = await ImagePicker().pickImage(
  source: ImageSource.gallery,
  maxWidth: 1000,
  maxHeight: 1000,
  imageQuality: 80,
);

// 从相机拍照
final XFile? image = await ImagePicker().pickImage(
  source: ImageSource.camera,
  preferredCameraDevice: CameraDevice.front,
);
3.3.2 pickMultiImage - 选择多张图片
dart 复制代码
Future<List<XFile>> pickMultiImage({
  double? maxWidth,                      // 可选:图片最大宽度
  double? maxHeight,                     // 可选:图片最大高度
  int? imageQuality,                     // 可选:图片质量 (0-100)
  bool requestFullMetadata = true,       // 可选:是否请求完整元数据
})

参数说明:

参数 类型 必填 默认值 说明
maxWidth double? null 图片最大宽度
maxHeight double? null 图片最大高度
imageQuality int? null 图片质量 (0-100)
requestFullMetadata bool true 是否获取完整元数据

返回值: Future<List<XFile>> - 返回选中的图片文件列表,用户取消时返回空列表

注意: 此方法在 iOS 14 以下版本不支持,OpenHarmony 平台完全支持。

3.3.3 pickVideo - 选择视频
dart 复制代码
Future<XFile?> pickVideo({
  required ImageSource source,           // 必填:图片来源
  CameraDevice preferredCameraDevice = CameraDevice.rear, // 可选:优先使用的相机
  Duration? maxDuration,                 // 可选:最大录制时长
})

参数说明:

参数 类型 必填 默认值 说明
source ImageSource - 图片来源:camera 或 gallery
preferredCameraDevice CameraDevice CameraDevice.rear 优先使用的相机
maxDuration Duration? null 最大录制时长,不限制则为 null

返回值: Future<XFile?> - 返回选中的视频文件对象,用户取消时返回 null

使用示例:

dart 复制代码
// 从相册选择视频
final XFile? video = await ImagePicker().pickVideo(
  source: ImageSource.gallery,
);

// 录制视频(最长 30 秒)
final XFile? video = await ImagePicker().pickVideo(
  source: ImageSource.camera,
  maxDuration: const Duration(seconds: 30),
);
3.3.4 pickMedia - 选择单个媒体(图片或视频)
dart 复制代码
Future<XFile?> pickMedia({
  double? maxWidth,                      // 可选:图片最大宽度
  double? maxHeight,                     // 可选:图片最大高度
  int? imageQuality,                     // 可选:图片质量 (0-100)
  bool requestFullMetadata = true,       // 可选:是否请求完整元数据
})

说明: 此方法允许用户从相册选择图片或视频,返回单个媒体文件。

返回值: Future<XFile?> - 返回选中的媒体文件对象,用户取消时返回 null

3.3.5 pickMultipleMedia - 选择多个媒体(图片或视频)
dart 复制代码
Future<List<XFile>> pickMultipleMedia({
  double? maxWidth,                      // 可选:图片最大宽度
  double? maxHeight,                     // 可选:图片最大高度
  int? imageQuality,                     // 可选:图片质量 (0-100)
  bool requestFullMetadata = true,       // 可选:是否请求完整元数据
})

说明: 此方法允许用户从相册同时选择多张图片和多个视频。

返回值: Future<List<XFile>> - 返回选中的媒体文件列表,用户取消时返回空列表

3.3.6 supportsImageSource - 检查是否支持图片来源
dart 复制代码
bool supportsImageSource(ImageSource source)

说明: 检查当前平台是否支持指定的图片来源。

返回值: bool - 支持返回 true,否则返回 false

3.4 XFile 文件对象

XFile 是跨平台文件抽象类,提供了文件操作的标准接口。

常用属性
属性 类型 说明
name String 文件名(包含扩展名)
path String? 文件路径(部分平台可能为 null)
length Future<int> 文件大小(字节)
mimeType String? 文件的 MIME 类型
常用方法
方法 返回值 说明
readAsBytes() Future<Uint8List> 读取文件内容为字节数组
readAsString() Future<String> 读取文件内容为字符串
openRead() Stream<Uint8List> 以流的方式读取文件
saveTo(String path) Future<void> 将文件保存到指定路径

四、完整使用示例

以下是一个完整的图片视频采集应用,整合了所有核心功能:

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'image_picker 示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6366F1)),
        useMaterial3: true,
      ),
      home: const ImagePickerHomePage(),
    );
  }
}

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

  @override
  State<ImagePickerHomePage> createState() => _ImagePickerHomePageState();
}

class _ImagePickerHomePageState extends State<ImagePickerHomePage> {
  final ImagePicker _picker = ImagePicker();
  XFile? _selectedImage;
  List<XFile> _selectedImages = [];
  XFile? _selectedVideo;
  List<XFile> _selectedMedias = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('image_picker 图片视频采集'),
        backgroundColor: const Color(0xFF6366F1),
        foregroundColor: Colors.white,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildSectionTitle('单张图片选择'),
          _buildButton(
            icon: Icons.photo_library,
            label: '从相册选择图片',
            onTap: _pickImageFromGallery,
          ),
          _buildButton(
            icon: Icons.camera_alt,
            label: '拍照',
            onTap: _takePhoto,
          ),
          if (_selectedImage != null) _buildImagePreview(_selectedImage!),
          
          const SizedBox(height: 24),
          _buildSectionTitle('多张图片选择'),
          _buildButton(
            icon: Icons.photo_library_outlined,
            label: '选择多张图片',
            onTap: _pickMultipleImages,
          ),
          if (_selectedImages.isNotEmpty) _buildMultipleImagePreview(),
          
          const SizedBox(height: 24),
          _buildSectionTitle('视频选择'),
          _buildButton(
            icon: Icons.video_library,
            label: '从相册选择视频',
            onTap: _pickVideoFromGallery,
          ),
          _buildButton(
            icon: Icons.videocam,
            label: '录制视频',
            onTap: _recordVideo,
          ),
          if (_selectedVideo != null) _buildVideoPreview(),
          
          const SizedBox(height: 24),
          _buildSectionTitle('混合媒体选择'),
          _buildButton(
            icon: Icons.library_add,
            label: '选择单个媒体',
            onTap: _pickMedia,
          ),
          _buildButton(
            icon: Icons.library_add_check,
            label: '选择多个媒体',
            onTap: _pickMultipleMedias,
          ),
          if (_selectedMedias.isNotEmpty) _buildMultipleMediaPreview(),
          
          const SizedBox(height: 32),
        ],
      ),
    );
  }

  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Text(
        title,
        style: const TextStyle(
          fontSize: 18,
          fontWeight: FontWeight.bold,
          color: Color(0xFF6366F1),
        ),
      ),
    );
  }

  Widget _buildButton({
    required IconData icon,
    required String label,
    required VoidCallback onTap,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: ElevatedButton.icon(
        onPressed: onTap,
        icon: Icon(icon, color: Colors.white),
        label: Text(label),
        style: ElevatedButton.styleFrom(
          backgroundColor: const Color(0xFF6366F1),
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(vertical: 14),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
    );
  }

  Widget _buildImagePreview(XFile image) {
    return Card(
      margin: const EdgeInsets.only(top: 12),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '选中的图片: ${image.name}',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 8),
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.file(
                File(image.path!),
                height: 200,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ),
            const SizedBox(height: 8),
            Text('路径: ${image.path}'),
            FutureBuilder<int>(
              future: image.length(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return Text('大小: ${_formatFileSize(snapshot.data!)}');
                }
                return const Text('大小: 计算中...');
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMultipleImagePreview() {
    return Card(
      margin: const EdgeInsets.only(top: 12),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '选中的图片: ${_selectedImages.length} 张',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 8),
            SizedBox(
              height: 150,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: _selectedImages.length,
                itemBuilder: (context, index) {
                  return Padding(
                    padding: const EdgeInsets.only(right: 8),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Image.file(
                        File(_selectedImages[index].path!),
                        width: 150,
                        height: 150,
                        fit: BoxFit.cover,
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildVideoPreview() {
    return Card(
      margin: const EdgeInsets.only(top: 12),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '选中的视频',
              style: TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 8),
            Text('文件名: ${_selectedVideo!.name}'),
            Text('路径: ${_selectedVideo!.path}'),
            FutureBuilder<int>(
              future: _selectedVideo!.length(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return Text('大小: ${_formatFileSize(snapshot.data!)}');
                }
                return const Text('大小: 计算中...');
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMultipleMediaPreview() {
    return Card(
      margin: const EdgeInsets.only(top: 12),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '选中的媒体: ${_selectedMedias.length} 个',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 8),
            SizedBox(
              height: 100,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: _selectedMedias.length,
                itemBuilder: (context, index) {
                  final media = _selectedMedias[index];
                  final isVideo = media.name.endsWith('.mp4') ||
                      media.name.endsWith('.mov') ||
                      media.name.endsWith('.avi');
                  return Padding(
                    padding: const EdgeInsets.only(right: 8),
                    child: Stack(
                      alignment: Alignment.center,
                      children: [
                        ClipRRect(
                          borderRadius: BorderRadius.circular(8),
                          child: Image.file(
                            File(media.path!),
                            width: 100,
                            height: 100,
                            fit: BoxFit.cover,
                          ),
                        ),
                        if (isVideo)
                          Container(
                            decoration: BoxDecoration(
                              color: Colors.black54,
                              shape: BoxShape.circle,
                            ),
                            padding: const EdgeInsets.all(8),
                            child: const Icon(
                              Icons.play_arrow,
                              color: Colors.white,
                              size: 24,
                            ),
                          ),
                      ],
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _formatFileSize(int bytes) {
    if (bytes < 1024) return '$bytes B';
    if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
    return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
  }

  Future<void> _pickImageFromGallery() async {
    final XFile? image = await _picker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 1920,
      maxHeight: 1080,
      imageQuality: 85,
    );
    if (image != null && mounted) {
      setState(() {
        _selectedImage = image;
      });
    }
  }

  Future<void> _takePhoto() async {
    final XFile? image = await _picker.pickImage(
      source: ImageSource.camera,
      preferredCameraDevice: CameraDevice.rear,
      imageQuality: 90,
    );
    if (image != null && mounted) {
      setState(() {
        _selectedImage = image;
      });
    }
  }

  Future<void> _pickMultipleImages() async {
    final List<XFile> images = await _picker.pickMultiImage(
      maxWidth: 1920,
      maxHeight: 1080,
      imageQuality: 85,
    );
    if (images.isNotEmpty && mounted) {
      setState(() {
        _selectedImages = images;
      });
    }
  }

  Future<void> _pickVideoFromGallery() async {
    final XFile? video = await _picker.pickVideo(
      source: ImageSource.gallery,
    );
    if (video != null && mounted) {
      setState(() {
        _selectedVideo = video;
      });
    }
  }

  Future<void> _recordVideo() async {
    final XFile? video = await _picker.pickVideo(
      source: ImageSource.camera,
      maxDuration: const Duration(seconds: 60),
    );
    if (video != null && mounted) {
      setState(() {
        _selectedVideo = video;
      });
    }
  }

  Future<void> _pickMedia() async {
    final XFile? media = await _picker.pickMedia();
    if (media != null && mounted) {
      setState(() {
        _selectedMedias = [media];
      });
    }
  }

  Future<void> _pickMultipleMedias() async {
    final List<XFile> medias = await _picker.pickMultipleMedia();
    if (medias.isNotEmpty && mounted) {
      setState(() {
        _selectedMedias = medias;
      });
    }
  }
}

五、适配要点

5.1 OpenHarmony 平台特性

  1. 权限处理

    • OpenHarmony 平台需要显式声明相机和媒体权限
    • 系统会在首次访问时自动弹出权限请求对话框
    • 权限被拒绝后,需要在系统设置中手动开启
  2. 文件路径处理

    • OpenHarmony 平台返回的文件路径为沙箱路径
    • 路径仅在应用生命周期内有效,不应跨会话保存
    • 如需持久化存储,应使用 saveTo 方法复制文件
  3. 图片压缩

    • maxWidthmaxHeight 参数会保持原始宽高比
    • imageQuality 参数仅对 JPEG 格式有效
    • HEIC 格式在 OpenHarmony 平台的处理与 Android 有所不同
  4. 相机设备选择

    • preferredCameraDevice 参数在部分设备上可能被忽略
    • 建议在使用前检查设备是否支持指定相机

5.2 与 Android/iOS 的差异

差异点 Android/iOS OpenHarmony
权限申请 运行时动态申请 声明式权限,首次使用时弹窗
文件路径 持久化路径 沙箱临时路径
HEIC 支持 iOS 支持,Android 8 不支持 支持,但需配合尺寸修改使用
元数据获取 完整 EXIF 信息 部分元数据可能需要额外权限
视频格式 多种格式支持 主要支持 MP4 格式

5.3 注意事项

  1. 不要保存文件路径

    • XFile 的路径仅在应用会话内有效
    • 跨会话使用应复制文件到持久化目录
  2. 错误处理

    • 所有方法都可能抛出 PlatformException
    • 建议使用 try-catch 包裹调用代码
  3. 内存管理

    • 大图片建议使用 maxWidth/maxHeight 限制
    • 多选时注意内存占用,必要时进行压缩
  4. UI 线程

    • 图片选择器会阻塞 UI,直到用户完成选择
    • 建议在异步方法中调用

六、常见问题

Q1: 调用相机拍照时应用崩溃?

原因: 未配置相机权限或权限被拒绝。

解决方案:

  1. 检查 module.json5 中是否添加了 ohos.permission.CAMERA 权限
  2. 确保在 string.json 中添加了权限说明
  3. 在系统设置中检查应用权限是否已授予

Q2: 选择的图片无法显示?

原因: 文件路径为沙箱临时路径,可能已被清理。

解决方案:

dart 复制代码
// 错误做法:直接保存路径
String path = image.path!; // 跨会话可能失效

// 正确做法:复制文件到持久化目录
import 'package:path_provider/path_provider.dart';

final directory = await getApplicationDocumentsDirectory();
final newPath = '${directory.path}/${image.name}';
await image.saveTo(newPath);

Q3: 图片压缩不生效?

原因: imageQuality 参数仅对 JPEG 格式有效,PNG 格式不支持压缩。

解决方案:

  • 确认选择的图片格式是否为 JPEG
  • 如果需要对 PNG 进行压缩,可以先转换为 JPEG:
dart 复制代码
final XFile? image = await _picker.pickImage(
  source: ImageSource.gallery,
  imageQuality: 80, // 仅对 JPEG 有效
);

Q4: 多选图片时返回空列表?

原因: 用户取消了选择或相册中没有符合条件的图片。

解决方案:

dart 复制代码
final List<XFile> images = await _picker.pickMultiImage();
if (images.isEmpty) {
  // 用户取消或没有选择图片
  return;
}
// 处理选中的图片

Q5: 视频录制时长限制不生效?

原因: maxDuration 参数在某些设备上可能被系统相机忽略。

解决方案:

  • maxDuration 是建议值,系统相机可能不严格遵守
  • 建议在录制完成后检查视频时长并进行处理

Q6: 如何判断选中的文件是图片还是视频?

解决方案:

dart 复制代码
bool isVideoFile(XFile file) {
  final extension = file.name.split('.').last.toLowerCase();
  return ['mp4', 'mov', 'avi', 'mkv', 'webm'].contains(extension);
}

// 使用示例
if (isVideoFile(media)) {
  // 处理视频
} else {
  // 处理图片
}

Q7: 应用重启后如何恢复之前选择的图片?

解决方案:

dart 复制代码
// 保存时复制文件到持久化目录
Future<String> saveImagePermanently(XFile image) async {
  final directory = await getApplicationDocumentsDirectory();
  final newPath = '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_${image.name}';
  await image.saveTo(newPath);
  return newPath;
}

// 使用时从持久化目录加载
Image.file(File(savedPath));

七、总结

image_picker 是 Flutter 生态中最常用的图片视频采集插件,在 OpenHarmony 平台的适配已经非常成熟。通过本文的介绍,你应该已经掌握了:

  1. image_picker 的核心 API 和使用方法
  2. OpenHarmony 平台的权限配置要求
  3. 完整的图片视频采集应用实现
  4. 平台适配的注意事项和常见问题解决方案

在实际开发中,建议根据具体需求选择合适的 API,并注意文件路径的临时性特点。对于需要持久化存储的场景,务必使用 saveTo 方法将文件复制到应用的持久化目录中。

💡 提示: 更多 OpenHarmony 适配的 Flutter 三方库信息,请访问 开源鸿蒙跨平台开发者社区 获取最新资源和技术支持。

相关推荐
2601_949593655 小时前
Flutter_OpenHarmony_三方库_fluttertoast消息提示适配详解
flutter
seabirdssss5 小时前
Flutter 开发环境配置
android·windows·flutter·adb
2601_949593655 小时前
Flutter_OpenHarmony_三方库_webview_flutter网页内容嵌入与交互适配详解
flutter·harmonyos
hqyjzsb6 小时前
传统剪辑师升级AI视频生成师后接单效率与收入变化
人工智能·aigc·服务发现·音视频·学习方法·业界资讯·ai写作
tangweiguo030519876 小时前
Flutter 分页缓存实战:基于 Riverpod 的 SWR 策略实现
flutter
byte轻骑兵6 小时前
【LE Audio】ASCS精讲[7]: SDP互操作落地,蓝牙音频服务发现全解析
人工智能·音视频·le audio·低功耗音频·ascs
Ww.xh7 小时前
鸿蒙Flutter混合开发实战:跨平台UI无缝集成
flutter·华为·harmonyos
SoulRed7 小时前
Android Studio 调试flutter gradle的问题
android·flutter·android studio
blanks20207 小时前
为 Zed 编辑器 添加 flutter dart snippets
前端·flutter