🚀运行效果展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Flutter框架跨平台鸿蒙开发——饮食热量查询APP的开发流程

📝 前言

随着移动应用开发的快速发展,跨平台开发框架越来越受到开发者的青睐。Flutter作为Google推出的开源UI框架,凭借其"Write Once, Run Anywhere"的理念,以及优秀的性能和丰富的生态,成为了跨平台开发的热门选择。

华为鸿蒙系统(HarmonyOS)作为一款面向全场景的分布式操作系统,具有强大的设备互联能力和统一的开发框架。将Flutter与鸿蒙系统结合,既可以利用Flutter的跨平台优势,又能充分发挥鸿蒙系统的特性,为开发者和用户带来更好的体验。

本文将详细介绍如何使用Flutter框架开发一款跨平台的饮食热量查询APP,并成功在鸿蒙系统上运行。我们将从项目架构设计、核心功能实现、跨平台适配等方面,全面展示开发流程和技术要点。

🥗 应用介绍

应用概述

饮食热量查询APP是一款帮助用户查询各种食物热量和营养成分的工具型应用。用户可以通过搜索或分类浏览的方式,快速找到所需食物的详细营养信息,包括热量、蛋白质、碳水化合物和脂肪等。

目标用户

  • 关注健康饮食的人群
  • 正在减肥或健身的用户
  • 需要控制饮食的慢性病患者
  • 营养搭配师和健身教练

核心功能

  1. 食物列表展示:以卡片形式展示食物列表,包含名称、热量和分类信息
  2. 搜索功能:支持按食物名称搜索
  3. 分类筛选:支持按食物分类筛选
  4. 食物详情:展示食物的详细营养成分
  5. 添加食物:允许用户手动添加新的食物

🛠️ 开发环境搭建

1. Flutter环境搭建

  • 安装Flutter SDK:从Flutter官网下载并安装最新版本的Flutter SDK
  • 配置环境变量:将Flutter SDK的bin目录添加到系统环境变量中
  • 验证安装:运行flutter doctor命令,确保所有依赖项都已正确安装

2. 鸿蒙开发环境配置

  • 安装DevEco Studio:从华为开发者官网下载并安装DevEco Studio
  • 配置鸿蒙SDK:在DevEco Studio中下载并配置鸿蒙SDK
  • 安装Flutter插件:在DevEco Studio中安装Flutter插件,支持Flutter开发

3. 项目创建

使用Flutter命令行工具创建项目:

flutter create calorie_tracker

🏗️ 项目架构设计

分层架构

本项目采用经典的三层架构设计,确保代码的可维护性和可扩展性:

UI层

服务层

数据层

数据库

模块划分

项目按照功能模块进行划分,主要包含以下模块:

lib/
├── calorie_tracker/
│   ├── models/          # 数据模型
│   ├── services/        # 业务逻辑
│   ├── screens/         # 页面组件
│   └── utils/           # 工具类
├── screens/             # 主页面
└── main.dart            # 应用入口

🔧 核心功能实现

1. 数据模型设计

Food模型:定义食物的数据结构

/// 食物数据模型
class Food {
  /// 食物ID
  final int? id;
  /// 食物名称
  final String name;
  /// 热量 (每100克/毫升)
  final double calories;
  /// 蛋白质 (每100克/毫升)
  final double protein;
  /// 碳水化合物 (每100克/毫升)
  final double carbs;
  /// 脂肪 (每100克/毫升)
  final double fat;
  /// 食物类型
  final String category;

  /// 构造函数
  Food({
    this.id,
    required this.name,
    required this.calories,
    required this.protein,
    required this.carbs,
    required this.fat,
    required this.category,
  });

  /// 从Map转换为Food对象
  factory Food.fromMap(Map<String, dynamic> map) {
    return Food(
      id: map['id'],
      name: map['name'],
      calories: map['calories'],
      protein: map['protein'],
      carbs: map['carbs'],
      fat: map['fat'],
      category: map['category'],
    );
  }

  /// 转换为Map对象
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'calories': calories,
      'protein': protein,
      'carbs': carbs,
      'fat': fat,
      'category': category,
    };
  }
}

2. 服务层实现

FoodService:处理食物数据的业务逻辑

/// 食物服务类,处理食物数据的业务逻辑
class FoodService {
  /// 内存中的食物列表
  final List<Food> _foods = [
    Food(
      id: 1,
      name: '苹果',
      calories: 52,
      protein: 0.3,
      carbs: 13.8,
      fat: 0.2,
      category: '水果',
    ),
    // 其他食物数据...
  ];

  /// 获取所有食物
  Future<List<Food>> getAllFoods() async {
    return _foods;
  }

  /// 根据名称搜索食物
  Future<List<Food>> searchFoods(String keyword) async {
    return _foods
        .where((food) => food.name
            .toLowerCase()
            .contains(keyword.toLowerCase()))
        .toList();
  }

  /// 根据分类获取食物
  Future<List<Food>> getFoodsByCategory(String category) async {
    return _foods.where((food) => food.category == category).toList();
  }

  /// 添加新食物
  Future<int> addFood(Food food) async {
    final newId = _foods.length + 1;
    final newFood = Food(
      id: newId,
      name: food.name,
      calories: food.calories,
      protein: food.protein,
      carbs: food.carbs,
      fat: food.fat,
      category: food.category,
    );
    _foods.add(newFood);
    return newId;
  }

  /// 获取所有食物分类
  Future<List<String>> getAllCategories() async {
    return _foods.map((food) => food.category).toSet().toList();
  }
}

3. UI层实现

食物列表页面
/// 食物列表页面
class FoodListScreen extends StatefulWidget {
  /// 构造函数
  const FoodListScreen({Key? key}) : super(key: key);

  
  State<FoodListScreen> createState() => _FoodListScreenState();
}

class _FoodListScreenState extends State<FoodListScreen> {
  /// 食物服务实例
  final FoodService _foodService = FoodService();
  /// 食物列表
  List<Food> _foods = [];
  /// 筛选后的食物列表
  List<Food> _filteredFoods = [];
  /// 所有分类
  List<String> _categories = [];
  /// 搜索关键词
  String _searchKeyword = '';
  /// 当前选中的分类
  String? _selectedCategory;
  /// 是否加载中
  bool _isLoading = true;

  
  void initState() {
    super.initState();
    _loadData();
  }

  /// 加载数据
  Future<void> _loadData() async {
    try {
      setState(() {
        _isLoading = true;
      });

      // 加载食物和分类
      final foods = await _foodService.getAllFoods();
      final categories = await _foodService.getAllCategories();

      setState(() {
        _foods = foods;
        _filteredFoods = foods;
        _categories = categories;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      _showError('加载数据失败');
    }
  }

  // 其他方法...

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('饮食热量查询'),
        backgroundColor: Colors.blueAccent,
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : Column(
              children: [
                // 搜索栏
                Padding(
                  padding: const EdgeInsets.all(10.0),
                  child: TextField(
                    onChanged: _searchFoods,
                    decoration: InputDecoration(
                      hintText: '搜索食物...',
                      prefixIcon: const Icon(Icons.search),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(20.0),
                      ),
                    ),
                  ),
                ),
                // 分类筛选
                SizedBox(
                  height: 50,
                  child: ListView.builder(
                    scrollDirection: Axis.horizontal,
                    itemCount: _categories.length + 1,
                    itemBuilder: (context, index) {
                      final String category = index == 0
                          ? '全部'
                          : _categories[index - 1];
                      final bool isSelected = _selectedCategory == category ||
                          (index == 0 && _selectedCategory == null);

                      return Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 5.0),
                        child: ChoiceChip(
                          label: Text(category),
                          selected: isSelected,
                          onSelected: (selected) {
                            if (selected) {
                              _selectCategory(index == 0 ? null : category);
                            }
                          },
                          selectedColor: Colors.blueAccent,
                          labelStyle: TextStyle(
                            color: isSelected ? Colors.white : Colors.black,
                          ),
                        ),
                      );
                    },
                  ),
                ),
                // 食物列表
                Expanded(
                  child: ListView.builder(
                    itemCount: _filteredFoods.length,
                    itemBuilder: (context, index) {
                      final food = _filteredFoods[index];
                      return Card(
                        margin: const EdgeInsets.all(8.0),
                        child: ListTile(
                          title: Text(food.name),
                          subtitle: Text(
                            '${food.calories} 大卡/100g | ${food.category}',
                          ),
                          trailing: const Icon(Icons.arrow_forward_ios),
                          onTap: () => _navigateToFoodDetail(food),
                        ),
                      );
                    },
                  ),
                ),
              ],
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _navigateToAddFood,
        backgroundColor: Colors.blueAccent,
        child: const Icon(Icons.add),
        tooltip: '添加食物',
      ),
    );
  }
}
食物详情页面
/// 食物详情页面
class FoodDetailScreen extends StatelessWidget {
  /// 食物对象
  final Food food;

  /// 构造函数
  const FoodDetailScreen({Key? key, required this.food}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(food.name),
        backgroundColor: Colors.blueAccent,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 食物名称和分类
            Text(
              food.name,
              style: const TextStyle(
                fontSize: 24.0,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8.0),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
              decoration: BoxDecoration(
                color: Colors.blueAccent.withOpacity(0.2),
                borderRadius: BorderRadius.circular(16.0),
              ),
              child: Text(
                food.category,
                style: TextStyle(
                  fontSize: 14.0,
                  color: Colors.blueAccent,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ),
            const SizedBox(height: 24.0),

            // 热量信息卡片
            Card(
              elevation: 2.0,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(12.0),
              ),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    const Text(
                      '热量信息',
                      style: TextStyle(
                        fontSize: 18.0,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    const SizedBox(height: 16.0),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          '${food.calories}',
                          style: const TextStyle(
                            fontSize: 48.0,
                            fontWeight: FontWeight.bold,
                            color: Colors.blueAccent,
                          ),
                        ),
                        const SizedBox(width: 8.0),
                        const Text(
                          '大卡/100g',
                          style: TextStyle(
                            fontSize: 16.0,
                            color: Colors.grey,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 24.0),

            // 营养成分信息
            const Text(
              '营养成分 (每100克)',
              style: TextStyle(
                fontSize: 18.0,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16.0),

            // 蛋白质
            _buildNutrientItem('蛋白质', food.protein, 'g', Colors.blue),
            const SizedBox(height: 12.0),

            // 碳水化合物
            _buildNutrientItem('碳水化合物', food.carbs, 'g', Colors.green),
            const SizedBox(height: 12.0),

            // 脂肪
            _buildNutrientItem('脂肪', food.fat, 'g', Colors.orange),
          ],
        ),
      ),
    );
  }

  /// 构建营养成分项
  Widget _buildNutrientItem(String name, double value, String unit, Color color) {
    return Card(
      elevation: 2.0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12.0),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              name,
              style: TextStyle(
                fontSize: 16.0,
                fontWeight: FontWeight.w500,
              ),
            ),
            Row(
              children: [
                Text(
                  '$value',
                  style: TextStyle(
                    fontSize: 18.0,
                    fontWeight: FontWeight.bold,
                    color: color,
                  ),
                ),
                const SizedBox(width: 4.0),
                Text(
                  unit,
                  style: TextStyle(
                    fontSize: 14.0,
                    color: Colors.grey,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

4. 导航设计

将饮食热量查询APP添加到主页面的功能卡片列表中:

// 饮食热量查询功能卡片
_buildFunctionCard(
  context: context,
  title: '饮食热量查询',
  description: '查询各种食物的热量和营养成分',
  icon: Icons.food_bank,
  color: Colors.green,
  onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) =>
            const FoodListScreen(),
      ),
    );
  },
),

🔄 跨平台适配

1. 鸿蒙系统特殊处理

在开发过程中,我们遇到了SQLite数据库在鸿蒙系统上的兼容性问题。为了解决这个问题,我们采用了内存数据列表作为临时解决方案,确保应用能够正常运行。

2. 响应式布局

应用采用了响应式布局设计,确保在不同尺寸的鸿蒙设备上都能有良好的显示效果:

// 使用Expanded和Flexible组件实现响应式布局
Expanded(
  child: ListView.builder(
    // 列表内容
  ),
),

// 使用MediaQuery获取屏幕尺寸
final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = screenWidth / 2 - 20;

3. 主题适配

应用支持鸿蒙系统的主题切换,确保与系统主题保持一致:

// 使用Theme.of(context)获取当前主题
Theme.of(context).primaryColor,
Theme.of(context).textTheme.headline6,

🐛 测试与调试

1. 遇到的问题

  1. SQLite数据库兼容性问题:在鸿蒙系统上,SQLite数据库初始化失败,导致数据无法加载
  2. 调试服务连接问题:Flutter DevTools与鸿蒙设备的连接不稳定
  3. 布局溢出问题:在某些鸿蒙设备上,部分页面出现布局溢出

2. 解决方案

  1. SQLite数据库问题:将数据存储方式从SQLite数据库改为内存中的数据列表
  2. 调试服务问题:使用flutter run --verbose命令获取详细日志,调整调试参数
  3. 布局溢出问题:使用SingleChildScrollViewExpanded组件优化布局,确保内容能够自适应屏幕尺寸

📊 开发流程总结

开发流程图

项目初始化

环境搭建

架构设计

数据模型设计

服务层实现

UI层实现

导航设计

跨平台适配

测试与调试

应用发布

技术栈总结

技术类别 技术栈
开发框架 Flutter 3.6.2
编程语言 Dart
状态管理 原生StatefulWidget
数据存储 内存数据列表
UI组件 Material Design
目标平台 HarmonyOS

🎯 总结与展望

总结

本文详细介绍了使用Flutter框架开发跨平台鸿蒙应用的完整流程,以饮食热量查询APP为例,展示了从项目初始化、架构设计到核心功能实现的全过程。我们遇到了SQLite数据库兼容性、调试服务连接等问题,并通过相应的解决方案成功解决。

通过本次开发实践,我们验证了Flutter框架在鸿蒙系统上的可行性,同时也积累了跨平台开发的经验。Flutter的跨平台优势和鸿蒙系统的特性相结合,为移动应用开发提供了新的思路和可能性。

展望

  1. 数据持久化优化:进一步研究SQLite数据库在鸿蒙系统上的兼容性问题,实现真正的数据持久化
  2. 功能扩展:添加更多功能,如饮食记录、热量统计、营养分析等
  3. 性能优化:优化应用性能,减少内存占用和启动时间
  4. 多端适配:进一步优化在不同鸿蒙设备上的显示效果
  5. 鸿蒙特性集成:集成鸿蒙系统的分布式能力,实现多设备协同

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐