Flutter for OpenHarmony 衣橱管家App实战 - 智能推荐实现

最近在做一个衣橱管理类的App,其中有个功能我觉得挺有意思的,就是根据用户选择的场合和天气,智能推荐今天应该穿什么。

这篇文章就来聊聊这个智能推荐功能是怎么实现的。

需求分析

做这个功能之前,我先想了想用户的使用场景。

早上起床,打开衣柜不知道穿啥,这时候如果App能根据今天要去的场合和天气情况,给出一套搭配建议,那就太方便了。

所以这个页面需要做的事情就三件:

  • 让用户选场合

  • 让用户选天气

  • 生成推荐结果

页面结构搭建

先把页面的基本框架搭起来。

StatefulWidget是因为页面上有好几个状态需要管理。

dart 复制代码
class _OutfitRecommendScreenState extends State<OutfitRecommendScreen> {
  String _selectedOccasion = '日常';
  String _selectedWeather = '晴天';
  List<ClothingItem> _recommendedItems = [];

这里定义了三个状态变量。

_selectedOccasion存用户选中的场合,_selectedWeather存天气。

默认值给的是"日常"和"晴天",毕竟大多数情况下就是这样。

接下来是选项数据的定义:

dart 复制代码
  final List<String> _occasions = [
    '日常', 
    '工作', 
    '约会', 
    '运动', 
    '聚会'
  ];
  
  final List<String> _weathers = [
    '晴天', 
    '阴天', 
    '雨天', 
    '寒冷', 
    '炎热'
  ];

场合有5种,天气也是5种,基本覆盖了日常使用场景。

final修饰是因为这些数据初始化后不会再变。

后续如果要扩展,直接往数组里加就行。

页面整体布局

页面分上下两部分,上面是筛选条件,下面是推荐结果。

dart 复制代码
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('智能推荐')
    ),
    body: Column(
      children: [
        _buildFilterSection(),
        Expanded(
          child: _buildRecommendation()
        ),
      ],
    ),
  );
}

Column把两部分垂直排列。

Expanded让推荐结果区域占满剩余空间。

这样不管筛选区域多高,下面的内容都能自适应。

筛选条件区域

页面上半部分是让用户选条件的区域。

我把它封装成了_buildFilterSection方法。

dart 复制代码
Widget _buildFilterSection() {
  return Card(
    margin: EdgeInsets.all(16.w),
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 内容
        ],
      ),
    ),
  );
}

外层用Card包裹,加点阴影效果看起来更有层次感。

crossAxisAlignment设成start让内容左对齐。

内边距用16.w,这是用了flutter_screenutil做的适配。

场合选择的标题部分:

dart 复制代码
Text(
  '选择场合', 
  style: TextStyle(
    fontSize: 14.sp, 
    fontWeight: FontWeight.bold
  )
),

SizedBox(height: 8.h),

标题用粗体显示,让用户一眼就能看到这是干嘛的。

下面加个间距,让布局不那么拥挤。

sph都是屏幕适配的单位。

场合选择用ChoiceChip来实现:

dart 复制代码
Wrap(
  spacing: 8.w,
  children: _occasions.map((o) {
    return ChoiceChip(
      label: Text(o),
      selected: _selectedOccasion == o,
      selectedColor: const Color(0xFFE91E63),
      labelStyle: TextStyle(
        color: _selectedOccasion == o 
          ? Colors.white 
          : Colors.black87
      ),
      onSelected: (s) => setState(() => _selectedOccasion = o),
    );
  }).toList(),
),

ChoiceChip自带选中效果,用起来很方便。

Wrap组件可以让这些Chip自动换行,不用担心屏幕宽度不够。

点击的时候调用setState更新状态,界面就会自动刷新。

天气选择的实现方式差不多:

dart 复制代码
Text(
  '今日天气', 
  style: TextStyle(
    fontSize: 14.sp, 
    fontWeight: FontWeight.bold
  )
),

SizedBox(height: 8.h),

Wrap(
  spacing: 8.w,
  children: _weathers.map((w) {
    return ChoiceChip(
      label: Text(w),
      selected: _selectedWeather == w,
      selectedColor: Colors.blue,
      labelStyle: TextStyle(
        color: _selectedWeather == w 
          ? Colors.white 
          : Colors.black87
      ),
      onSelected: (s) => setState(() => _selectedWeather = w),
    );
  }).toList(),
),

和场合选择的区别就是换了个颜色,用蓝色来区分。

场合用粉色,天气用蓝色,视觉上能一眼区分开。

这种小细节能提升用户体验,不容易搞混。

最后是生成推荐的按钮:

dart 复制代码
SizedBox(height: 16.h),

SizedBox(
  width: double.infinity,
  child: ElevatedButton(
    onPressed: _generateRecommendation,
    style: ElevatedButton.styleFrom(
      backgroundColor: const Color(0xFFE91E63)
    ),
    child: const Text(
      '生成推荐', 
      style: TextStyle(color: Colors.white)
    ),
  ),
),

按钮设成全宽,点击区域大一些用户操作更方便。

颜色用主题色保持统一,和场合选中的颜色一致。

点击后会调用_generateRecommendation方法生成推荐。

推荐结果展示

下半部分展示推荐结果。

要处理两种情况:空状态和有数据。

dart 复制代码
Widget _buildRecommendation() {
  if (_recommendedItems.isEmpty) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.auto_awesome, 
            size: 64.sp, 
            color: Colors.grey
          ),
          SizedBox(height: 16.h),
          Text(
            '点击"生成推荐"获取穿搭建议', 
            style: TextStyle(
              fontSize: 16.sp, 
              color: Colors.grey
            )
          ),
        ],
      ),
    );
  }
  
  // 有数据时的展示...
}

空状态的处理很重要,不能让用户看到一片空白。

这里放了个图标加文字提示,告诉用户下一步该做什么。

用灰色表示这是个引导状态,不是主要内容。

有推荐结果时,先搭建外层结构:

dart 复制代码
return SingleChildScrollView(
  padding: EdgeInsets.all(16.w),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        '为您推荐', 
        style: TextStyle(
          fontSize: 18.sp, 
          fontWeight: FontWeight.bold
        )
      ),
      SizedBox(height: 16.h),
      // 推荐内容...
    ],
  ),
);

SingleChildScrollView包裹,内容多的时候可以滚动。

标题"为您推荐"用大号粗体,突出显示。

整体左对齐,符合阅读习惯。

推荐结果的容器用渐变背景:

dart 复制代码
Container(
  padding: EdgeInsets.all(16.w),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [
        const Color(0xFFE91E63).withOpacity(0.1), 
        Colors.blue.withOpacity(0.1)
      ],
    ),
    borderRadius: BorderRadius.circular(12.r),
  ),
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      // 衣物卡片...
    ],
  ),
),

渐变从粉色到蓝色,和上面的选项颜色呼应。

整体视觉效果比较统一,不会显得突兀。

圆角设成12让卡片看起来更柔和。

单个衣物卡片的展示:

dart 复制代码
Column(
  children: [
    Container(
      width: 80.w,
      height: 100.h,
      decoration: BoxDecoration(
        color: ClothingItem
          .getColorFromName(item.color)
          .withOpacity(0.3),
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Center(
        child: Icon(
          Icons.checkroom, 
          size: 40.sp, 
          color: ClothingItem.getColorFromName(item.color)
        ),
      ),
    ),
    SizedBox(height: 8.h),
    Text(
      item.name, 
      style: TextStyle(fontSize: 12.sp), 
      textAlign: TextAlign.center
    ),
    Text(
      item.category, 
      style: TextStyle(
        fontSize: 10.sp, 
        color: Colors.grey
      )
    ),
  ],
),

每件衣物显示一个色块加图标,下面是名称和分类。

色块的颜色取的是衣物本身的颜色属性。

用户一眼就能知道推荐的是什么颜色的衣服。

推荐理由展示

光给推荐结果还不够,得告诉用户为什么推荐这套。

dart 复制代码
SizedBox(height: 24.h),

Text(
  '推荐理由', 
  style: TextStyle(
    fontSize: 16.sp, 
    fontWeight: FontWeight.bold
  )
),

SizedBox(height: 8.h),

和推荐结果之间留点间距,视觉上分开两个区块。

标题同样用粗体,保持风格统一。

这部分内容放在Card里面展示。

理由项的通用组件:

dart 复制代码
Widget _buildReasonItem(
  IconData icon, 
  String title, 
  String desc
) {
  return Padding(
    padding: EdgeInsets.only(bottom: 12.h),
    child: Row(
      children: [
        Icon(
          icon, 
          color: const Color(0xFFE91E63), 
          size: 20.sp
        ),
        SizedBox(width: 12.w),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title, 
              style: TextStyle(
                fontSize: 14.sp, 
                fontWeight: FontWeight.bold
              )
            ),
            Text(
              desc, 
              style: TextStyle(
                fontSize: 12.sp, 
                color: Colors.grey
              )
            ),
          ],
        ),
      ],
    ),
  );
}

这是个通用的理由项组件,左边图标右边文字。

封装成方法后可以复用,传入不同参数就能显示不同内容。

图标用主题色,和整体风格保持一致。

调用的时候这样写:

dart 复制代码
Card(
  child: Padding(
    padding: EdgeInsets.all(16.w),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        _buildReasonItem(
          Icons.event, 
          '场合适配', 
          '适合$_selectedOccasion场合穿着'
        ),
        _buildReasonItem(
          Icons.wb_sunny, 
          '天气适宜', 
          '适合$_selectedWeather天气'
        ),
        _buildReasonItem(
          Icons.palette, 
          '色彩搭配', 
          '颜色协调,整体和谐'
        ),
      ],
    ),
  ),
),

用字符串插值把用户选的场合和天气动态显示出来。

让推荐理由更有针对性,不是千篇一律的文案。

三个理由分别从场合、天气、色彩三个维度说明。

核心推荐算法

最后是推荐算法的实现。

目前的逻辑比较简单,先把框架搭好。

dart 复制代码
void _generateRecommendation() {
  final provider = Provider.of<WardrobeProvider>(
    context, 
    listen: false
  );
  final clothes = provider.clothes;
  final random = Random();

  List<ClothingItem> recommended = [];

通过Provider获取衣橱数据。

listen: false表示不需要监听变化,只是取一次数据。

创建一个空列表来存放推荐结果。

筛选上衣:

dart 复制代码
  final tops = clothes
    .where((c) => c.category == '上衣')
    .toList();

  if (tops.isNotEmpty) {
    recommended.add(
      tops[random.nextInt(tops.length)]
    );
  }

where方法筛选出所有上衣。

如果有上衣,就随机选一件加到推荐列表。

random.nextInt生成一个随机索引。

筛选下装:

dart 复制代码
  final bottoms = clothes
    .where((c) => c.category == '裤子' || c.category == '裙子')
    .toList();

  if (bottoms.isNotEmpty) {
    recommended.add(
      bottoms[random.nextInt(bottoms.length)]
    );
  }

下装包括裤子和裙子两种类型。

同样是随机选一件加到推荐列表。

这样就组成了一套上下搭配。

更新状态:

dart 复制代码
  setState(() {
    _recommendedItems = recommended;
  });
}

调用setState更新推荐列表。

界面会自动刷新显示新的推荐结果。

这个算法后续可以优化,比如根据天气筛选季节、根据场合筛选标签等。

底部操作按钮

页面底部有两个按钮,让用户可以换一套或者保存。

dart 复制代码
SizedBox(height: 16.h),

Row(
  children: [
    Expanded(
      child: OutlinedButton(
        onPressed: _generateRecommendation,
        child: const Text('换一套'),
      ),
    ),
    SizedBox(width: 12.w),
    Expanded(
      child: ElevatedButton(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('已保存为搭配')
            )
          );
        },
        style: ElevatedButton.styleFrom(
          backgroundColor: const Color(0xFFE91E63)
        ),
        child: const Text(
          '保存搭配', 
          style: TextStyle(color: Colors.white)
        ),
      ),
    ),
  ],
),

"换一套"重新调用推荐算法,用户不满意可以一直换。

"保存搭配"目前只是弹个提示,后续可以接入真正的保存逻辑。

两个按钮用Expanded平分宽度,一个描边一个实心,视觉上有主次之分。

小结

这个智能推荐功能的实现不复杂,核心就是状态管理和数据筛选。

ChoiceChip做多选一很合适,Provider跨组件共享数据也很方便。

目前的推荐算法比较简单,但框架搭好了,后续想加更复杂的逻辑只需要改_generateRecommendation方法就行,其他地方不用动。


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

相关推荐
菜鸟小芯2 小时前
【开源鸿蒙跨平台开发先锋训练营】DAY8~DAY13 底部选项卡&我的页面功能实现
flutter·harmonyos
灰灰勇闯IT2 小时前
Flutter for OpenHarmony:悬浮按钮(FloatingActionButton)最佳实践 —— 强化核心操作,提升用户效率
flutter·华为·交互
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony “心流之泉”——在碎片洪流中,为你筑一眼专注的清泉
开发语言·前端·flutter·交互
一起养小猫3 小时前
Flutter for OpenHarmony 进阶:表达式解析算法与计算器核心实现
算法·flutter·harmonyos
不爱吃糖的程序媛4 小时前
Flutter 三方库鸿蒙(OHOS)适配分析流程
flutter·华为·harmonyos
mocoding4 小时前
我这样用鸿蒙化Flutter三方库file_selector实现单图片和多图片选择
flutter·华为·harmonyos
牛马1114 小时前
flutter Riverpod 中的 overrideWith
android·java·flutter
牛马1114 小时前
flutter riverpod AsyncNotifier 和 Notifier
flutter
不爱吃糖的程序媛5 小时前
如何判断Flutter三方库是否需要OHOS适配开发?附完整适配指导
flutter·华为·harmonyos