从零开始搭建一个支持三端的 Flutter 应用

前言

做跨平台开发这么多年,最怕的就是项目做到一半发现架构不合理,重构起来痛不欲生。这次做 CleanMark AI 项目,我花了整整两天时间做技术选型和架构设计,后面的开发过程证明这两天没白费。

这篇文章我会把整个决策过程分享给你,包括为什么选择某个技术,为什么放弃另一个技术,以及踩过的坑。


一、为什么选择 Flutter + HarmonyOS

1.1 业务需求分析

先说说项目背景。CleanMark AI 是一个 AI 去水印工具,核心功能是:

  • 图片去水印(1积分/次)
  • 视频去水印(5积分/次)
  • 积分系统(看广告赚积分)
  • 历史记录管理

关键需求:

  • 需要同时支持 Android、iOS、HarmonyOS 三端
  • UI 要求高(深色主题、渐变效果、动画)
  • 需要调用原生能力(相机、相册、视频播放)
  • 要集成华为广告 SDK
    请添加图片描述

1.2 技术方案对比

我当时考虑了三种方案:

方案 优点 缺点 结论
原生开发 性能最好,体验最佳 三端代码量 3 倍,维护成本高 ❌ 团队只有 2 个人,搞不定
React Native 生态成熟,社区活跃 HarmonyOS 支持不完善 ❌ 适配成本太高
Flutter 一套代码三端运行,性能接近原生 HarmonyOS 需要自己适配插件 ✅ 最终选择

为什么选 Flutter?

  1. 性能够用:60fps 的 UI 渲染,对于我们这种工具类应用完全够了
  2. 开发效率高:Hot Reload 真的能省很多时间
  3. HarmonyOS 官方支持:华为提供了 Flutter SDK for HarmonyOS
  4. 插件生态:虽然 HarmonyOS 插件少,但可以自己写

1.3 HarmonyOS 适配的挑战

选择 Flutter 不代表没有坑,HarmonyOS 适配主要有三个难点:

难点1:插件适配

Flutter 官方插件大多不支持 HarmonyOS,需要自己写平台代码。比如:

  • image_picker - 需要用 ArkTS 调用 HarmonyOS 的 Picker API
  • video_player - 需要封装 AVPlayer
  • shared_preferences - 需要用 Preferences API

难点2:ArkTS 语法限制

HarmonyOS NEXT 使用 ArkTS(TypeScript 的子集),有很多限制:

  • 不支持 Function.bind()
  • 不支持对象解构
  • 不支持 Spread 操作符
  • 类型检查非常严格

难点3:调试困难

HarmonyOS 的错误信息不如 Android 清晰,经常遇到:

THREAD_BLOCK_6S: Main thread blocked for 6 seconds

这种错误,排查起来很费时间。


二、Clean Architecture 架构设计

2.1 为什么需要架构

很多人觉得小项目不需要架构,直接写就行。但我的经验是:没有架构的项目,后期维护成本是有架构的 3-5 倍

CleanMark AI 虽然只有 12 个页面,但功能不简单:

  • 用户认证
  • 积分系统
  • 图片/视频处理
  • 历史记录
  • 广告集成

如果不做好分层,代码很快就会变成一团乱麻。

2.2 Clean Architecture 三层结构

我采用了 Uncle Bob 的 Clean Architecture,简化成三层:

┌─────────────────────────────────────────┐
│         Presentation Layer              │
│  (UI + State Management + Navigation)   │
│                                         │
│  - Pages (页面)                         │
│  - Widgets (组件)                       │
│  - Providers (状态管理)                 │
└─────────────────────────────────────────┘
              ↓ 只能依赖
┌─────────────────────────────────────────┐
│           Domain Layer                  │
│      (Business Logic + Entities)        │
│                                         │
│  - Entities (实体)                      │
│  - Use Cases (用例)                     │
│  - Repository Interfaces (仓库接口)     │
└─────────────────────────────────────────┘
              ↓ 只能依赖
┌─────────────────────────────────────────┐
│            Data Layer                   │
│   (Data Sources + Repository Impl)      │
│                                         │
│  - Models (数据模型)                    │
│  - Data Sources (数据源)                │
│  - Repository Implementations (仓库实现)│
└─────────────────────────────────────────┘

核心原则:依赖倒置

  • Presentation 层只能依赖 Domain 层
  • Domain 层不能依赖任何层(纯 Dart,无 Flutter 依赖)
  • Data 层实现 Domain 层定义的接口

2.3 项目目录结构

基于 Clean Architecture,我设计了这样的目录结构:

lib/
├── app/                          # 应用层
│   ├── app.dart                  # MaterialApp 配置
│   ├── router.dart               # 路由配置
│   ├── theme.dart                # 主题配置
│   └── app_colors.dart           # 颜色常量
│
├── core/                         # 核心层(跨功能共享)
│   ├── constants/                # 常量
│   │   └── api_constants.dart    # API 路径
│   ├── network/                  # 网络层
│   │   └── api_client.dart       # Dio 封装
│   ├── platform/                 # 平台适配层
│   │   ├── image_picker_service.dart
│   │   └── storage_service.dart
│   ├── services/                 # 共享服务
│   │   ├── inpaint_service.dart  # 去水印服务
│   │   └── task_service.dart     # 任务管理
│   └── utils/                    # 工具类
│       ├── app_prefs.dart        # 本地存储
│       ├── mask_generator.dart   # 遮罩生成
│       └── video_utils.dart      # 视频工具
│
├── features/                     # 功能模块(按业务领域划分)
│   ├── auth/                     # 认证模块
│   │   ├── auth_service.dart
│   │   ├── login_screen.dart
│   │   ├── user_model.dart
│   │   └── user_provider.dart
│   │
│   ├── home/                     # 首页模块
│   │   └── home_screen.dart
│   │
│   ├── image/                    # 图片处理模块
│   │   ├── image_upload_screen.dart
│   │   ├── image_comparison_screen.dart
│   │   └── widgets/
│   │       └── paint_canvas.dart
│   │
│   ├── video/                    # 视频处理模块
│   │   ├── video_upload_screen.dart
│   │   └── video_result_screen.dart
│   │
│   ├── history/                  # 历史记录模块
│   │   ├── history_list_screen.dart
│   │   ├── history_detail_screen.dart
│   │   ├── history_service.dart
│   │   ├── task_model.dart
│   │   └── widgets/
│   │       └── record_card.dart
│   │
│   ├── points/                   # 积分模块
│   │   ├── points_history_screen.dart
│   │   ├── earn_points_screen.dart
│   │   └── ad_service.dart
│   │
│   └── onboarding/               # 引导页模块
│       └── onboarding_screen.dart
│
└── main.dart                     # 应用入口

为什么这样设计?

  1. 按功能划分,不按类型划分

    • ❌ 不要:lib/screens/, lib/widgets/, lib/models/
    • ✅ 推荐:lib/features/auth/, lib/features/home/
  2. 每个功能模块独立

    • 模块内的文件只被模块内使用
    • 跨模块共享的放到 core/shared/
  3. 平台相关代码统一封装

    • 所有 Platform Channel 调用都在 core/platform/
    • 业务代码不直接调用原生 API

三、技术栈选型

3.1 状态管理:Riverpod

候选方案对比:

方案 优点 缺点 选择
Provider 简单易用,官方推荐 样板代码多,类型安全差
Bloc 架构清晰,适合大型项目 学习曲线陡,代码量大
Riverpod 类型安全,编译时检查,代码简洁 相对较新,社区小

为什么选 Riverpod?

// Provider 的问题:需要 BuildContext
final user = Provider.of<User>(context);

// Riverpod 的优势:不需要 BuildContext
final user = ref.watch(userProvider);

Riverpod 的核心优势:

  1. 编译时类型检查:Provider 不存在会编译报错
  2. 不依赖 BuildContext:可以在任何地方使用
  3. 自动依赖管理:Provider 之间的依赖关系自动处理
  4. 测试友好:可以轻松 mock Provider

实际使用示例:

// 定义 Provider
final userProvider = StateNotifierProvider<UserNotifier, User?>((ref) {
  return UserNotifier();
});

// 在 Widget 中使用
class HomeScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);

    return Text('积分: ${user?.credits ?? 0}');
  }
}

3.2 路由管理:go_router

为什么不用 Navigator?

Flutter 自带的 Navigator 有几个问题:

  • 路由配置分散,难以维护
  • 不支持深链接
  • 路由守卫实现复杂

go_router 的优势:

// 集中式路由配置
final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const IndexScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/main',
      builder: (context, state) => const MainScreen(),
      redirect: (context, state) {
        // 路由守卫:未登录跳转到登录页
        final isLoggedIn = /* 检查登录状态 */;
        return isLoggedIn ? null : '/login';
      },
    ),
  ],
);

// 页面跳转
context.go('/main');
context.push('/image-upload');

实际项目中的路由配置:

// lib/app/router.dart
final routerProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authStateProvider);

  return GoRouter(
    initialLocation: '/',
    redirect: (context, state) {
      final isLoggedIn = authState.isLoggedIn;
      final isLoginRoute = state.matchedLocation == '/login';

      // 未登录且不在登录页,跳转到登录页
      if (!isLoggedIn && !isLoginRoute) {
        return '/login';
      }

      // 已登录且在登录页,跳转到首页
      if (isLoggedIn && isLoginRoute) {
        return '/main';
      }

      return null;
    },
    routes: [
      // ... 路由配置
    ],
  );
});

四、开发环境搭建

4.1 Flutter SDK 安装

系统要求:

  • macOS / Windows / Linux
  • 磁盘空间:至少 2.8 GB
  • Git 已安装

安装步骤:

# 1. 下载 Flutter SDK
git clone https://github.com/flutter/flutter.git -b stable

# 2. 配置环境变量(macOS/Linux)
export PATH="$PATH:`pwd`/flutter/bin"

# 3. 运行 flutter doctor 检查环境
flutter doctor

# 4. 安装 HarmonyOS 支持
flutter config --enable-ohos

常见问题:

# 问题1:Android licenses 未接受
flutter doctor --android-licenses

# 问题2:Xcode 未安装(macOS)
# 从 App Store 安装 Xcode

# 问题3:网络问题(国内)
# 配置镜像源
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

4.2 DevEco Studio 安装(HarmonyOS 开发)

下载地址:
https://developer.huawei.com/consumer/cn/deveco-studio/

安装步骤:

  1. 下载 DevEco Studio 安装包
  2. 安装并启动
  3. 配置 HarmonyOS SDK
  4. 创建模拟器或连接真机

配置 Flutter 项目支持 HarmonyOS:

# 1. 创建 Flutter 项目
flutter create my_app

# 2. 添加 HarmonyOS 平台支持
cd my_app
flutter create --platforms=ohos .

# 3. 查看项目结构
ls -la
# 会看到新增的 ohos/ 目录

本篇小结

这篇文章我们完成了:

  1. ✅ 技术选型:Flutter + Riverpod + go_router
  2. ✅ 架构设计:Clean Architecture 三层结构
  3. ✅ 目录规划:按功能模块划分
  4. ✅ 环境搭建:Flutter SDK + DevEco Studio

下一篇我们会深入讲解路由配置和状态管理的实战应用。


思考题

  1. 为什么 Domain 层不能依赖 Flutter 框架?
  2. 如果你的项目只支持 Android 和 iOS,还需要 Clean Architecture 吗?
  3. Riverpod 和 Bloc 各适合什么场景?

欢迎在评论区分享你的想法!


下一篇预告:第02篇 - 路由与状态管理实战

Logo

一站式 AI 云服务平台

更多推荐