1. 插件介绍

desktop_photo_search 是一个功能强大的 Flutter 桌面照片搜索应用,专为鸿蒙跨平台开发设计。该应用通过集成 Unsplash API,为开发者提供了完整的高质量照片搜索解决方案。经过鸿蒙化适配后,该库可以在 OpenHarmony 平台上无缝运行,支持 Windows、macOS、Linux 等多个桌面平台。

核心功能特性

  • 高质量照片搜索:通过 Unsplash API 搜索数百万张免费高质量照片
  • 双重界面风格:同时提供 Material Design 和 Fluent UI 两种界面实现
  • 灵活搜索参数:支持按关键词、方向、颜色等多种条件筛选照片
  • 本地下载功能:支持将照片保存到本地文件系统
  • 完整数据模型:包含照片信息、用户信息、位置信息等丰富的数据结构
  • 鸿蒙深度适配:针对 OpenHarmony API 9+ 进行了专项优化

技术架构

该应用采用了现代化的 Flutter 技术栈:

  • 状态管理:基于 Provider 实现响应式状态管理
  • 网络请求:使用 http 包与 Unsplash API 通信
  • 本地存储:通过 file_selector 实现跨平台文件操作
  • UI 框架:支持 Material 和 Fluent UI 两种设计语言
  • 数据序列化:使用 built_value 进行类型安全的数据序列化

2. 环境要求

在开始使用 desktop_photo_search 之前,请确保您的开发环境满足以下要求:

  • OpenHarmony SDK:API Version 9 及以上版本
  • Flutter SDK:3.7.0 及以上版本,推荐使用 3.13.0 以上版本以获得更好的鸿蒙支持
  • Dart SDK:3.1.0 及以上版本
  • 开发工具:DevEco Studio 4.0 及以上版本,或 VS Code + OpenHarmony 插件

开发环境验证

在开始开发之前,建议您验证开发环境是否正确配置。运行以下命令来检查环境状态:

# 检查 Flutter 版本
flutter --version

# 检查 OpenHarmony 设备连接
hdc list targets

# 创建新的鸿蒙项目
flutter create --platforms ohos my_photo_app

3. 安装与配置

3.1 创建鸿蒙项目

首先,创建一个新的 Flutter 鸿蒙项目作为照片搜索应用的基础:

flutter create --platforms ohos desktop_photo_search_demo
cd desktop_photo_search_demo

3.2 添加依赖配置

在项目的 pubspec.yaml 文件中添加 desktop_photo_search 及相关依赖。由于这是一个自定义修改版本的库,需要通过 Git 形式引入:

# pubspec.yaml

name: desktop_photo_search_demo
description: A Flutter photo search application for OpenHarmony.
version: 1.0.0+1

environment:
  sdk: ^3.1.0
  flutter: ^3.13.0

dependencies:
  flutter:
    sdk: flutter

  # 照片搜索核心库
  desktop_photo_search:
    git:
      url: "https://atomgit.com/your-org/desktop_photo_search"
      path: "material"

  # 必要的依赖包
  http: ^1.1.0
  provider: ^6.0.5
  transparent_image: ^2.0.1
  url_launcher: ^6.1.12
  uuid: ^4.0.0

  # 界面风格相关
  cupertino_icons: ^1.0.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

3.3 安装依赖

执行以下命令安装所有依赖包:

flutter pub get

如果遇到依赖冲突或版本问题,可以尝试使用以下命令:

flutter pub upgrade --major-versions
flutter pub get

3.4 配置 Unsplash API 访问密钥

要使用 desktop_photo_search 的照片搜索功能,您需要从 Unsplash 获取 API 访问密钥:

步骤 1:注册 Unsplash 开发者账号
  1. 访问 Unsplash 开发者平台
  2. 点击 “Your Apps” 菜单
  3. 点击 “New Application” 按钮创建新应用
步骤 2:获取 API 密钥

创建应用后,在应用详情页面找到 Access Key,这是调用 API 所需的认证凭证。请妥善保管此密钥,不要将其上传到公开的代码仓库。

步骤 3:配置密钥文件

在项目的 lib 目录下创建 unsplash_access_key.dart 文件:

// lib/unsplash_access_key.dart

const String unsplashAccessKey = 'YOUR_UNSPLASH_ACCESS_KEY';

YOUR_UNSPLASH_ACCESS_KEY 替换为您从 Unsplash 获取的实际访问密钥。

3.5 安全提示

为了保护您的 API 密钥,建议将密钥文件添加到 .gitignore 文件中:

# .gitignore

lib/unsplash_access_key.dart
*.keystore
*.jks
*.apk
*.aab

4. API 使用详解

4.1 初始化 Unsplash 客户端

在应用启动时,首先需要初始化 Unsplash API 客户端。以下是完整的初始化代码:

import 'package:desktop_photo_search/src/unsplash/unsplash.dart';
import 'unsplash_access_key.dart';

class PhotoService {
  late final Unsplash _unsplash;

  PhotoService() {
    _initClient();
  }

  void _initClient() {
    _unsplash = Unsplash(
      accessKey: unsplashAccessKey,
      timeout: Duration(seconds: 30),
    );
  }

  Unsplash get client => _unsplash;
}

4.2 照片搜索功能

使用 Unsplash 客户端进行照片搜索,支持多种搜索参数:

import 'package:desktop_photo_search/src/unsplash/unsplash.dart';
import 'package:desktop_photo_search/src/unsplash/photo.dart';
import 'package:desktop_photo_search/src/unsplash/search_photos_response.dart';

class PhotoSearchService {
  final Unsplash _unsplash;

  PhotoSearchService(this._unsplash);

  Future<List<Photo>> searchPhotos({
    required String query,
    int page = 1,
    int perPage = 15,
    SearchPhotosOrientation? orientation,
    String? color,
    String? locale,
  }) async {
    try {
      final response = await _unsplash.searchPhotos(
        query: query,
        page: page,
        perPage: perPage,
        orientation: orientation,
        color: color,
        locale: locale,
      );

      if (response != null && response.results != null) {
        return response.results!;
      }

      return [];
    } catch (e) {
      print('搜索照片时发生错误: $e');
      return [];
    }
  }

  Future<List<Photo>> searchNaturePhotos() async {
    return searchPhotos(
      query: 'nature',
      perPage: 20,
      orientation: SearchPhotosOrientation.landscape,
    );
  }

  Future<List<Photo>> searchArchitecturePhotos() async {
    return searchPhotos(
      query: 'architecture',
      perPage: 20,
      orientation: SearchPhotosOrientation.portrait,
    );
  }
}

4.3 处理搜索结果

Unsplash API 返回的搜索结果包含丰富的照片信息,以下是处理结果的示例代码:

void processSearchResults(List<Photo> photos) {
  for (final photo in photos) {
    // 获取照片基本信息
    final String id = photo.id;
    final String? description = photo.description;
    final String? altDescription = photo.altDescription;

    // 获取照片尺寸
    final int width = photo.width ?? 0;
    final int height = photo.height ?? 0;

    // 获取照片 URL
    final String regularUrl = photo.urls?.regular ?? '';
    final String thumbUrl = photo.urls?.thumb ?? '';
    final String fullUrl = photo.urls?.full ?? '';

    // 获取用户信息
    final String? userName = photo.user?.name;
    final String? userLink = photo.user?.links?.html;

    // 获取统计信息
    final int likes = photo.likes ?? 0;
    final int? downloads = photo.downloads;

    // 获取位置信息
    final double? latitude = photo.location?.position?.latitude;
    final double? longitude = photo.location?.position?.longitude;
    final String? locationName = photo.location?.name;

    // 打印照片信息
    print('=' * 50);
    print('照片 ID: $id');
    print('描述: ${description ?? altDescription ?? "无描述"}');
    print('尺寸: ${width}x${height}');
    print('点赞数: $likes');
    print('下载数: ${downloads ?? "未知"}');
    print('用户: $userName');
    if (locationName != null) {
      print('位置: $locationName ($latitude, $longitude)');
    }
    print('URL: $regularUrl');
    print('=' * 50);
  }
}

4.4 照片下载功能

将搜索到的照片下载到本地存储:

import 'dart:io';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import 'package:file_selector/file_selector.dart';
import 'package:desktop_photo_search/src/unsplash/photo.dart';
import 'package:desktop_photo_search/src/unsplash/unsplash.dart';

class PhotoDownloadService {
  final Unsplash _unsplash;
  final String _defaultSavePath;

  PhotoDownloadService(this._unsplash, {String? savePath})
      : _defaultSavePath = savePath ?? '';

  Future<String?> downloadPhoto(Photo photo, {String? savePath}) async {
    try {
      // 获取照片数据
      final Uint8List imageBytes = await _unsplash.download(photo);

      // 确定保存路径
      final String filePath = savePath ?? await _getSavePath(photo);

      if (filePath.isEmpty) {
        print('用户取消了保存操作');
        return null;
      }

      // 保存文件
      final File file = File(filePath);
      await file.writeAsBytes(imageBytes);

      print('照片已成功保存到: $filePath');
      return filePath;
    } catch (e) {
      print('下载照片时发生错误: $e');
      return null;
    }
  }

  Future<String> _getSavePath(Photo photo) async {
    final String extension = _getFileExtension(photo.urls?.regular ?? 'jpg');
    final String fileName = '${photo.id}.$extension';

    final String? path = await getSavePath(
      suggestedName: fileName,
      acceptedTypeGroups: [
        XTypeGroup(
          label: 'Images',
          extensions: ['jpg', 'jpeg', 'png', 'webp'],
          mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
        ),
      ],
    );

    return path ?? '';
  }

  String _getFileExtension(String url) {
    final uri = Uri.parse(url);
    final path = uri.path;
    final extension = path.split('.').last.toLowerCase();
    return ['jpg', 'jpeg', 'png', 'webp'].contains(extension) ? extension : 'jpg';
  }

  Future<String> _getDefaultDownloadDirectory() async {
    if (Platform.isWindows) {
      return '${Platform.environment['USERPROFILE']}\\Downloads';
    } else if (Platform.isMacOS) {
      return '${Platform.environment['HOME']}/Downloads';
    } else {
      final directory = await getDownloadsDirectory();
      return directory?.path ?? '/tmp';
    }
  }
}

4.5 批量下载功能

支持批量下载多张照片到指定目录:

class BatchDownloadService {
  final PhotoDownloadService _downloadService;
  final String _downloadDirectory;

  BatchDownloadService(this._downloadService, {String? directory})
      : _downloadDirectory = directory ?? '';

  Future<DownloadResult> downloadMultiplePhotos(
    List<Photo> photos, {
    bool skipExisting = true,
    void Function(int, int, Photo)? onProgress,
  }) async {
    final results = DownloadResult();
    final total = photos.length;

    for (int i = 0; i < total; i++) {
      final photo = photos[i];
      final fileName = '${photo.id}.jpg';
      final filePath = '$_downloadDirectory/$fileName';

      // 检查文件是否已存在
      if (skipExisting && await File(filePath).exists()) {
        results.skipped++;
        onProgress?.call(i + 1, total, photo);
        continue;
      }

      // 下载照片
      final result = await _downloadService.downloadPhoto(
        photo,
        savePath: filePath,
      );

      if (result != null) {
        results.success++;
      } else {
        results.failed++;
      }

      onProgress?.call(i + 1, total, photo);
    }

    return results;
  }
}

class DownloadResult {
  int success = 0;
  int failed = 0;
  int skipped = 0;

  
  String toString() {
    return '下载完成 - 成功: $success, 失败: $failed, 跳过: $skipped';
  }
}

5. 界面实现示例

5.1 Material Design 风格界面

使用 Material Design 风格构建照片搜索界面:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'src/model/photo_search_model.dart';
import 'src/widgets/photo_search_content.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => PhotoSearchModel(),
        ),
      ],
      child: const PhotoSearchApp(),
    ),
  );
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '照片搜索',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

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

class _HomePageState extends State<HomePage> {
  final TextEditingController _searchController = TextEditingController();

  
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('照片搜索'),
        actions: [
          IconButton(
            icon: const Icon(Icons.info_outline),
            onPressed: () => _showUnsplashNotice(context),
          ),
        ],
      ),
      body: Column(
        children: [
          _buildSearchBar(context),
          const Expanded(
            child: PhotoSearchContent(),
          ),
        ],
      ),
    );
  }

  Widget _buildSearchBar(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _searchController,
              decoration: const InputDecoration(
                hintText: '搜索照片...',
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(),
              ),
              onSubmitted: (query) {
                if (query.isNotEmpty) {
                  context.read<PhotoSearchModel>().search(query);
                }
              },
            ),
          ),
          const SizedBox(width: 8),
          ElevatedButton.icon(
            onPressed: () {
              final query = _searchController.text;
              if (query.isNotEmpty) {
                context.read<PhotoSearchModel>().search(query);
              }
            },
            icon: const Icon(Icons.search),
            label: const Text('搜索'),
          ),
        ],
      ),
    );
  }

  void _showUnsplashNotice(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('关于照片来源'),
        content: const Text(
          '照片由 Unsplash 提供。'
          '请遵守 Unsplash 的使用条款。'
          '下载的照片仅限个人使用。',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }
}

5.2 照片网格展示组件

使用网格布局展示搜索结果照片:

import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
import 'src/unsplash/photo.dart';

class PhotoGrid extends StatelessWidget {
  final List<Photo> photos;
  final void Function(Photo) onPhotoTap;
  final void Function(Photo) onDownloadTap;

  const PhotoGrid({
    super.key,
    required this.photos,
    required this.onPhotoTap,
    required this.onDownloadTap,
  });

  
  Widget build(BuildContext context) {
    if (photos.isEmpty) {
      return const Center(
        child: Text('暂无搜索结果'),
      );
    }

    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
        childAspectRatio: 0.75,
      ),
      itemCount: photos.length,
      itemBuilder: (context, index) {
        final photo = photos[index];
        return _buildPhotoCard(context, photo);
      },
    );
  }

  Widget _buildPhotoCard(BuildContext context, Photo photo) {
    final String imageUrl = photo.urls?.regular ?? '';
    final String thumbUrl = photo.urls?.thumb ?? '';

    return Card(
      clipBehavior: Clip.antiAlias,
      child: Stack(
        fit: StackFit.expand,
        children: [
          // 加载缩略图
          FadeInImage.memoryNetwork(
            placeholder: kTransparentImage,
            image: thumbUrl,
            fit: BoxFit.cover,
          ),
          // 加载完成后显示高清图
          Image.network(
            imageUrl,
            fit: BoxFit.cover,
            loadingBuilder: (context, child, progress) {
              if (progress == null) return child;
              return const Center(
                child: CircularProgressIndicator(),
              );
            },
          ),
          // 照片信息覆盖层
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            child: Container(
              padding: const EdgeInsets.all(8),
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.bottomCenter,
                  end: Alignment.topCenter,
                  colors: [Colors.black87, Colors.transparent],
                ),
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Expanded(
                    child: Text(
                      photo.user?.name ?? 'Unknown',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                      ),
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                  Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: const Icon(
                          Icons.download,
                          color: Colors.white,
                          size: 20,
                        ),
                        onPressed: () => onDownloadTap(photo),
                      ),
                      IconButton(
                        icon: const Icon(
                          Icons.visibility,
                          color: Colors.white,
                          size: 20,
                        ),
                        onPressed: () => onPhotoTap(photo),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

6. 完整应用示例

以下是一个完整的应用示例,展示了如何整合所有功能:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:desktop_photo_search/material/lib/src/model/photo_search_model.dart';
import 'package:desktop_photo_search/material/lib/src/unsplash/photo.dart';
import 'package:desktop_photo_search/material/lib/src/unsplash/unsplash.dart';
import 'unsplash_access_key.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => PhotoSearchModel(),
        ),
      ],
      child: const MyPhotoApp(),
    ),
  );
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 照片搜索',
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const PhotoSearchHomePage(),
    );
  }
}

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

  
  State<PhotoSearchHomePage> createState() => _PhotoSearchHomePageState();
}

class _PhotoSearchHomePageState extends State<PhotoSearchHomePage> {
  final TextEditingController _searchController = TextEditingController();
  final Unsplash _unsplash = Unsplash(accessKey: unsplashAccessKey);

  
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final model = context.watch<PhotoSearchModel>();

    return Scaffold(
      appBar: AppBar(
        title: const Text('照片搜索'),
        actions: [
          IconButton(
            icon: const Icon(Icons.info),
            onPressed: () => _showInfoDialog(context),
          ),
        ],
      ),
      body: Column(
        children: [
          _buildSearchSection(context, model),
          Expanded(
            child: _buildResultsSection(context, model),
          ),
        ],
      ),
    );
  }

  Widget _buildSearchSection(BuildContext context, PhotoSearchModel model) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _searchController,
              decoration: const InputDecoration(
                labelText: '搜索照片',
                hintText: '输入关键词,如:nature, architecture...',
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(),
              ),
              onSubmitted: (value) => _performSearch(context, model, value),
            ),
          ),
          const SizedBox(width: 12),
          ElevatedButton.icon(
            onPressed: model.isLoading
                ? null
                : () => _performSearch(context, model, _searchController.text),
            icon: model.isLoading
                ? const SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.search),
            label: const Text('搜索'),
          ),
        ],
      ),
    );
  }

  Widget _buildResultsSection(BuildContext context, PhotoSearchModel model) {
    if (model.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (model.hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            Text('搜索失败: ${model.errorMessage}'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => _performSearch(context, model, _searchController.text),
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    if (model.photos.isEmpty) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.photo_library_outlined, size: 64, color: Colors.grey),
            SizedBox(height: 16),
            Text('输入关键词开始搜索照片'),
          ],
        ),
      );
    }

    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
        childAspectRatio: 0.8,
      ),
      itemCount: model.photos.length,
      itemBuilder: (context, index) {
        final photo = model.photos[index];
        return _buildPhotoCard(context, photo);
      },
    );
  }

  Widget _buildPhotoCard(BuildContext context, Photo photo) {
    return GestureDetector(
      onTap: () => _showPhotoDetail(context, photo),
      child: Card(
        clipBehavior: Clip.antiAlias,
        child: Stack(
          fit: StackFit.expand,
          children: [
            Image.network(
              photo.urls?.regular ?? '',
              fit: BoxFit.cover,
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress == null) return child;
                return const Center(child: CircularProgressIndicator());
              },
              errorBuilder: (context, error, stackTrace) {
                return const Center(
                  child: Icon(Icons.broken_image, color: Colors.grey),
                );
              },
            ),
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.bottomCenter,
                    end: Alignment.topCenter,
                    colors: [Colors.black54, Colors.transparent],
                  ),
                ),
                child: Text(
                  photo.user?.name ?? 'Unknown',
                  style: const TextStyle(color: Colors.white),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _performSearch(BuildContext context, PhotoSearchModel model, String query) {
    if (query.trim().isNotEmpty) {
      model.search(query.trim());
    }
  }

  void _showPhotoDetail(BuildContext context, Photo photo) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Image.network(photo.urls?.regular ?? ''),
            const SizedBox(height: 8),
            Text('作者: ${photo.user?.name ?? "Unknown"}'),
            Text('描述: ${photo.description ?? "无描述"}'),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('关闭'),
          ),
        ],
      ),
    );
  }

  void _showInfoDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('关于'),
        content: const Text(
          '本应用使用 Unsplash API 搜索免费高质量照片。\n\n'
          '照片版权归原作者所有,仅限个人学习使用。',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }
}

7. 常见问题与解决方案

7.1 API 调用失败

如果遇到 API 调用失败的问题,请检查以下几点:

// 错误处理示例
Future<List<Photo>> safeSearchPhotos(Unsplash unsplash, String query) async {
  try {
    final response = await unsplash.searchPhotos(query: query);
    if (response?.results != null) {
      return response!.results!;
    }
    return [];
  } on SocketException catch (e) {
    print('网络连接失败: $e');
    return [];
  } on TimeoutException catch (e) {
    print('请求超时: $e');
    return [];
  } catch (e) {
    print('未知错误: $e');
    return [];
  }
}

7.2 图片加载失败

图片加载失败时使用占位图和错误处理:

Widget buildSafeImage(String url) {
  return Image.network(
    url,
    fit: BoxFit.cover,
    loadingBuilder: (context, child, progress) {
      if (progress == null) return child;
      return const Center(child: CircularProgressIndicator());
    },
    errorBuilder: (context, error, stackTrace) {
      return Container(
        color: Colors.grey[300],
        child: const Icon(Icons.broken_image, color: Colors.grey),
      );
    },
  );
}

7.3 性能优化建议

针对大量照片加载的性能优化:

// 使用缩略图列表 + 按需加载高清图
class OptimizedPhotoGrid extends StatelessWidget {
  final List<Photo> photos;

  const OptimizedPhotoGrid({super.key, required this.photos});

  
  Widget build(BuildContext context) {
    return GridView.builder(
      itemCount: photos.length,
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        childAspectRatio: 1.0,
      ),
      itemBuilder: (context, index) {
        final photo = photos[index];
        return OptimizedPhotoItem(photo: photo);
      },
    );
  }
}

class OptimizedPhotoItem extends StatelessWidget {
  final Photo photo;

  const OptimizedPhotoItem({super.key, required this.photo});

  
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: photo.urls?.thumb ?? '',
      placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
      errorWidget: (context, url, error) => const Icon(Icons.error),
      imageBuilder: (context, imageProvider) => Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: imageProvider,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

8. 总结

本文详细介绍了 Flutter 跨平台照片搜索库 desktop_photo_search 在鸿蒙系统上的使用方法和最佳实践。通过这篇文章,您应该已经掌握了:

  1. 环境配置:如何搭建 Flutter + OpenHarmony 开发环境
  2. 依赖管理:通过 AtomGit 正确引入 desktop_photo_search 依赖
  3. API 使用:Unsplash 照片搜索和下载的完整 API 调用方法
  4. 界面实现:Material Design 风格的界面构建技巧
  5. 性能优化:大量图片加载的性能优化方案

desktop_photo_search 作为一款成熟的照片搜索解决方案,经过鸿蒙化适配后,为开发者提供了稳定可靠的跨平台照片搜索能力。无论是构建个人照片管理应用,还是开发专业的图片搜索平台,这个库都能满足您的需求。

快速使用步骤

  1. pubspec.yaml 中添加 Git 形式的依赖
  2. 从 Unsplash 开发者平台获取 API 访问密钥
  3. 创建 unsplash_access_key.dart 文件并配置密钥
  4. 初始化 Unsplash 客户端
  5. 调用搜索和下载 API
  6. 根据需要选择 Material 或 Fluent UI 界面风格

通过这个 package,您可以快速构建一个功能完整的照片搜索应用,并在鸿蒙系统上流畅运行。

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐