Flutter 美妆教程应用的三端一体化开发实践

作者:maaath

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

前言

在移动应用开发领域,如何实现“一次开发,多端运行”一直是开发者追求的目标。Flutter 作为 Google 推出的跨平台 UI 框架,凭借其高性能、热重载和丰富的生态系统,已广泛应用于 iOS、Android、Web 等平台。随着 OpenHarmony 生态的蓬勃发展,Flutter for OpenHarmony 的出现让开发者能够将现有的 Flutter 应用快速迁移到鸿蒙设备上,实现真正的三端一体化开发。

本文将以一个美妆教程应用为例,详细介绍如何基于 Flutter 框架开发跨平台应用,并将其运行在 OpenHarmony 设备上。文章将从项目架构、数据模型、UI 组件设计、网络请求封装等方面进行深入讲解,帮助读者快速掌握 Flutter 跨平台开发的核心技能。

一、项目概述

1.1 项目背景

美妆教程应用是一个集教程浏览、分类筛选、用户中心于一体的综合性应用。用户可以通过应用学习各种美妆技巧,包括底妆、眼妆、唇妆、腮红、眉妆和修容等多个分类。每个教程都包含详细的步骤说明和配图,帮助用户轻松掌握化妆技巧。

1.2 技术栈

  • 框架:Flutter 3.x
  • 语言:Dart
  • HTTP 客户端:dio(已成功适配 OpenHarmony)
  • 目标平台:Android、iOS、OpenHarmony
  • 状态管理:StatefulWidget(演示用,生产环境建议使用 Provider/Bloc)

1.3 项目结构

flutter_beauty_app/
├── lib/
│   ├── main.dart          # 应用入口
│   ├── models/            # 数据模型
│   ├── services/          # 网络服务
│   ├── pages/             # 页面组件
│   └── widgets/            # 通用组件
├── pubspec.yaml           # 项目配置
└── analysis_options.yaml  # 代码规范

二、数据模型设计

2.1 模型定义

良好的数据模型是应用架构的基础。在本项目中,我们定义了以下核心模型:

// 教程步骤模型
class TutorialStep {
  final int stepNumber;       // 步骤编号
  final String title;         // 步骤标题
  final String description;   // 步骤描述
  final String imageUrl;       // 步骤配图
  final String tip;           // 小贴士

  TutorialStep({
    required this.stepNumber,
    required this.title,
    required this.description,
    required this.imageUrl,
    required this.tip,
  });
}

// 教程模型
class Tutorial {
  final String id;            // 教程ID
  final String title;         // 教程标题
  final String coverUrl;      // 封面图
  final String category;      // 分类
  final String difficulty;    // 难度等级
  final String duration;      // 耗时
  final String author;        // 作者
  final String authorAvatar;  // 作者头像
  final String description;   // 简介
  final List<TutorialStep> steps;  // 步骤列表
  final int likeCount;        // 点赞数
  final int collectCount;     // 收藏数
  final int viewCount;        // 浏览数
  double rating;              // 评分

  Tutorial({
    required this.id,
    // ... 其他字段
  });
}

// 用户模型
class UserModel {
  final String nickname;      // 昵称
  final String avatar;        // 头像
  final int followCount;       // 关注数
  final int fansCount;         // 粉丝数
  final int likeCount;         // 获赞数
  final int collectCount;      // 收藏数
  final String signature;      // 个性签名
}

2.2 模型设计原则

  1. 不可变性:使用 final 关键字确保数据不可变
  2. 可空性:合理使用可选参数和默认值
  3. 一致性:命名遵循 Dart 官方规范

三、网络服务封装

3.1 dio 库简介

dio 是一个强大的 Dart HTTP 客户端,支持拦截器、请求取消、文件上传/下载等功能。更重要的是,dio 已经完成了 OpenHarmony 平台的适配,可以无缝在鸿蒙设备上运行。

3.2 服务封装

class BeautyService {
  Dio? _dio;

  Dio get dio {
    _dio ??= Dio(BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ));
    return _dio!;
  }

  // 获取教程列表
  Future<List<Tutorial>> getTutorialList({
    String category = '',
    int page = 1,
    int pageSize = 10,
  }) async {
    // 模拟网络请求
    await Future.delayed(const Duration(milliseconds: 500));
    return _getMockTutorials(category);
  }

  // 获取用户信息
  Future<UserModel> getUserInfo() async {
    await Future.delayed(const Duration(milliseconds: 300));
    return UserModel(
      nickname: '美妆爱好者',
      avatar: 'https://picsum.photos/seed/avatar/100/100',
      followCount: 28,
      fansCount: 156,
      likeCount: 328,
      collectCount: 45,
      signature: '每天都要美美哒~',
    );
  }

  private List<Tutorial> _getMockTutorials(String category) {
    final tutorials = _createMockTutorials();
    if (category.isEmpty) return tutorials;
    return tutorials.where((t) => t.category == category).toList();
  }
}

3.3 实际项目中的网络请求示例

在实际项目中,网络请求通常会这样实现:

// 获取教程列表
Future<List<Tutorial>> getTutorialList({
  String category = '',
  int page = 1,
  int pageSize = 10,
}) async {
  try {
    final response = await dio.get('/tutorials', queryParameters: {
      'category': category,
      'page': page,
      'pageSize': pageSize,
    });

    if (response.statusCode == 200) {
      final List<dynamic> data = response.data['data'];
      return data.map((json) => Tutorial.fromJson(json)).toList();
    }
  } catch (e) {
    print('获取教程列表失败: $e');
  }
  return [];
}

// 点赞教程
Future<bool> likeTutorial(String tutorialId) async {
  try {
    final response = await dio.post('/tutorials/$tutorialId/like');
    return response.statusCode == 200;
  } catch (e) {
    print('点赞失败: $e');
  }
  return false;
}

四、UI 组件设计

4.1 主题配置

统一的主题配置是保持应用视觉一致性的关键:

class BeautyColors {
  static const Color primary = Color(0xFFE91E63);      // 主色调 - 玫红色
  static const Color primaryLight = Color(0xFFFCE4EC); // 浅粉色
  static const Color background = Color(0xFFFFF8F9);    // 背景色
  static const Color textPrimary = Color(0xFF333333);   // 主文字色
  static const Color textSecondary = Color(0xFF666666); // 次要文字色
  static const Color textHint = Color(0xFF999999);     // 提示文字色
  static const Color error = Color(0xFFF44336);        // 错误/点赞色
  static const Color warning = Color(0xFFFF9800);      // 警告/评分色
}

ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFE91E63)),
  useMaterial3: true,
  scaffoldBackgroundColor: const Color(0xFFFFF8F9),
)

4.2 热门教程卡片

热门教程列表采用横向滚动设计,每个卡片包含排名标识:

Widget _buildHotTutorialCard(int index) {
  final tutorial = _tutorialList[index];
  return GestureDetector(
    onTap: () => _showTutorialDetail(tutorial),
    child: Container(
      width: 160,
      margin: const EdgeInsets.only(right: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Stack(
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.network(
                  tutorial.coverUrl,
                  width: 160,
                  height: 130,
                  fit: BoxFit.cover,
                ),
              ),
              // 分类标签
              Positioned(
                top: 0,
                left: 0,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                  decoration: BoxDecoration(
                    color: BeautyColors.primary,
                    borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(8),
                      bottomRight: Radius.circular(4),
                    ),
                  ),
                  child: Text(
                    tutorial.category,
                    style: const TextStyle(color: Colors.white, fontSize: 10),
                  ),
                ),
              ),
              // 排名标识
              Positioned(
                top: 8,
                right: 8,
                child: Container(
                  width: 28,
                  height: 28,
                  decoration: BoxDecoration(
                    color: BeautyColors.warning,
                    borderRadius: BorderRadius.circular(14),
                  ),
                  child: Center(
                    child: Text(
                      '#${index + 1}',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 12,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
          // 标题和作者信息
          Text(
            tutorial.title,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    ),
  );
}

4.3 分类网格组件

分类入口采用 3x2 网格布局,每个分类配有独特的图标和颜色:

Widget _buildCategoryGrid() {
  final categoriesData = [
    {'name': '底妆', 'icon': '💄', 'color': '0xFFFFB6C1', 'count': '24'},
    {'name': '眼妆', 'icon': '👁️', 'color': '0xFF87CEEB', 'count': '32'},
    {'name': '唇妆', 'icon': '💋', 'color': '0xFFFF6B6B', 'count': '28'},
    {'name': '腮红', 'icon': '🌸', 'color': '0xFFFFA07A', 'count': '16'},
    {'name': '眉妆', 'icon': '✨', 'color': '0xFFDDA0DD', 'count': '20'},
    {'name': '修容', 'icon': '🎨', 'color': '0xFF98D8C8', 'count': '12'},
  ];

  return GridView.builder(
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3,
      childAspectRatio: 1,
      crossAxisSpacing: 12,
      mainAxisSpacing: 12,
    ),
    itemCount: categoriesData.length,
    itemBuilder: (context, index) {
      final item = categoriesData[index];
      return GestureDetector(
        onTap: () {
          setState(() {
            _selectedCategory = item['name']!;
            _currentTabIndex = 1;
          });
          _loadTutorials();
        },
        child: Container(
          decoration: BoxDecoration(
            color: Color(int.parse(item['color']!)).withAlpha(30),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(item['icon']!, style: const TextStyle(fontSize: 32)),
              Text(item['name']!),
              Text('${item['count']}个教程'),
            ],
          ),
        ),
      );
    },
  );
}

4.4 底部导航栏

底部 TabBar 实现页面的快速切换:

Widget _buildTabBar() {
  final tabs = [
    {'icon': '💄', 'selectedIcon': '✨', 'title': '推荐'},
    {'icon': '📚', 'selectedIcon': '📖', 'title': '教程'},
    {'icon': '❤️', 'selectedIcon': '💖', 'title': '收藏'},
    {'icon': '👤', 'selectedIcon': '👥', 'title': '我的'},
  ];

  return Container(
    height: 56,
    decoration: BoxDecoration(
      color: Colors.white,
      boxShadow: [
        BoxShadow(
          color: Colors.black.withAlpha(20),
          blurRadius: 20,
          offset: const Offset(0, -5),
        ),
      ],
    ),
    child: Row(
      children: tabs.asMap().entries.map((entry) {
        final index = entry.key;
        final tab = entry.value;
        final isSelected = _currentTabIndex == index;
        return Expanded(
          child: GestureDetector(
            onTap: () => setState(() => _currentTabIndex = index),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  isSelected ? tab['selectedIcon']! : tab['icon']!,
                  style: const TextStyle(fontSize: 24),
                ),
                Text(
                  tab['title']!,
                  style: TextStyle(
                    fontSize: 12,
                    color: isSelected ? BeautyColors.primary : BeautyColors.textHint,
                  ),
                ),
              ],
            ),
          ),
        );
      }).toList(),
    ),
  );
}

4.5 教程详情弹窗

点击教程卡片时,底部弹出详情页面:

void _showTutorialDetail(Tutorial tutorial) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (context) => DraggableScrollableSheet(
      initialChildSize: 0.9,
      maxChildSize: 0.95,
      minChildSize: 0.5,
      expand: false,
      builder: (context, scrollController) => SingleChildScrollView(
        controller: scrollController,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 封面图
            Image.network(tutorial.coverUrl, height: 200, fit: BoxFit.cover),
            // 标题
            Text(tutorial.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
            // 作者信息
            Row(
              children: [
                CircleAvatar(backgroundImage: NetworkImage(tutorial.authorAvatar)),
                Text(tutorial.author),
                const Spacer(),
                Text('❤️ ${tutorial.likeCount}'),
                Text('⭐ ${tutorial.rating}'),
              ],
            ),
            // 步骤列表
            ...tutorial.steps.map((step) => _buildStepCard(step)),
          ],
        ),
      ),
    ),
  );
}

五、分类筛选功能

5.1 分类标签组件

横向滚动的分类标签,支持"全部"、“底妆”、“眼妆”、"唇妆"等分类:

Widget _buildCategoryTabs() {
  final categories = ['全部', '底妆', '眼妆', '唇妆', '腮红', '眉妆', '修容'];

  return Container(
    height: 50,
    color: Colors.white,
    child: ListView.builder(
      scrollDirection: Axis.horizontal,
      itemCount: categories.length,
      itemBuilder: (context, index) {
        final category = categories[index];
        final isSelected = _selectedCategory == category;
        return GestureDetector(
          onTap: () {
            setState(() => _selectedCategory = category);
            _loadTutorials();
          },
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12),
                child: Text(
                  category,
                  style: TextStyle(
                    fontSize: 14,
                    color: isSelected ? BeautyColors.primary : BeautyColors.textSecondary,
                    fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                  ),
                ),
              ),
              AnimatedContainer(
                duration: const Duration(milliseconds: 200),
                width: isSelected ? 24 : 0,
                height: 3,
                decoration: BoxDecoration(
                  color: BeautyColors.primary,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ],
          ),
        );
      },
    ),
  );
}

5.2 筛选逻辑

Future<void> _loadTutorials({bool refresh = false}) async {
  final category = _selectedCategory == '全部' ? '' : _selectedCategory;
  final tutorials = await _service.getTutorialList(category: category);
  setState(() => _tutorialList = tutorials);
}

六、用户中心设计

6.1 用户信息卡片

展示用户头像、昵称、签名及统计数据:

Widget _buildUserCard() {
  final user = _userInfo ?? UserModel(/* 默认值 */);

  return Card(
    child: Column(
      children: [
        Row(
          children: [
            CircleAvatar(
              radius: 35,
              backgroundImage: NetworkImage(user.avatar),
              border: Border.all(color: BeautyColors.primary, width: 3),
            ),
            const SizedBox(width: 16),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(user.nickname),
                Text(user.signature),
              ],
            ),
            const Spacer(),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(
                border: Border.all(color: BeautyColors.primary),
                borderRadius: BorderRadius.circular(20),
              ),
              child: const Text('编辑'),
            ),
          ],
        ),
        const Divider(height: 1),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _buildStatItem(user.followCount.toString(), '关注'),
            _buildStatItem(user.fansCount.toString(), '粉丝'),
            _buildStatItem(user.likeCount.toString(), '获赞'),
            _buildStatItem(user.collectCount.toString(), '收藏'),
          ],
        ),
      ],
    ),
  );
}

6.2 功能菜单

Widget _buildMenuCard() {
  return Card(
    child: Column(
      children: [
        _buildMenuItem('📋', '浏览历史', '查看浏览记录'),
        _buildMenuItem('📝', '我的发布', '管理发布的教程'),
        _buildMenuItem('⭐', '我的收藏', '收藏的教程'),
        const Divider(height: 1),
        _buildMenuItem('⚙️', '设置', '应用设置与偏好'),
        _buildMenuItem('💡', '关于', '应用信息与帮助'),
      ],
    ),
  );
}

七、OpenHarmony 适配要点

7.1 权限配置

在鸿蒙设备上运行需要配置相关权限:

{
  "module": {
    "requestPermissions": [
      {"name": "ohos.permission.INTERNET"},
      {"name": "ohos.permission.GET_NETWORK_INFO"}
    ]
  }
}

7.2 注意事项

  1. 网络图片加载:使用 Image.network 时需要处理加载失败的情况
  2. 国际化:Flutter for OpenHarmony 支持 RTL 布局
  3. 平台特定代码:使用 Platform.isOpenHarmony 判断平台
// 处理网络图片加载失败
Image.network(
  tutorial.coverUrl,
  errorBuilder: (_, __, ___) => Container(
    color: Colors.grey[200],
    child: const Icon(Icons.image),
  ),
)

八、运行效果截图

以下是应用在 OpenHarmony 设备上的运行截图:

8.1 推荐页

> **请在此处插入推荐页截图**

推荐页展示热门教程轮播图、妆容分类入口和精选推荐内容。用户可以快速浏览各类美妆教程,并通过分类网格快速跳转到感兴趣的类别。

8.2 教程页

> **请在此处插入教程页截图**

教程页支持分类筛选,用户可以通过顶部标签快速切换不同类别的教程。每个教程卡片展示封面图、标题、难度等级、作者信息和点赞数等关键数据。

8.3 我的页

> **请在此处插入我的页截图**

用户中心展示个人资料、统计数据和功能入口。用户可以查看关注数、粉丝数、获赞数和收藏数,并通过菜单访问浏览历史、设置等功能。

九、代码仓库

本文涉及的完整代码已托管至 AtomGit:

  • Flutter 美妆教程应用仓库:https://atomgit.com/maaath/flutter-beauty-tutorial

十、总结

本文通过一个完整的 Flutter 美妆教程应用,详细介绍了跨平台应用开发的核心知识点,包括:

  1. 项目架构设计:清晰的分层结构,良好的模块划分
  2. 数据模型定义:类型安全的数据结构设计
  3. 网络服务封装:基于 dio 的网络请求封装
  4. UI 组件开发:可复用的 Flutter 组件设计
  5. 状态管理:基于 StatefulWidget 的简单状态管理
  6. OpenHarmony 适配:跨平台开发的注意事项

Flutter for OpenHarmony 的出现,为开发者提供了更多选择。通过 Flutter,开发者可以同时覆盖 Android、iOS 和 OpenHarmony 三大平台,大大降低了开发和维护成本。希望本文能为正在学习或准备进行跨平台开发的读者提供一些参考和帮助。

十一、参考资料

  1. Flutter 官方文档:https://flutter.cn/docs
  2. dio 官方文档:https://github.com/flutterchina/dio
  3. OpenHarmony 开发者文档:https://developer.harmonyos.com
  4. AtomGit 代码托管平台:https://atomgit.com

Logo

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

更多推荐