🚀运行效果展示

在这里插入图片描述

在这里插入图片描述

Flutter框架跨平台鸿蒙开发——每日日记APP的开发流程

📝 前言

随着移动应用开发的快速发展,跨平台开发框架成为了开发者们的首选。Flutter作为Google推出的开源UI工具包,以其高性能、高保真的UI渲染和"一次编写,多处运行"的特性,受到了广泛关注。同时,鸿蒙系统作为华为自主研发的分布式操作系统,也在快速发展,成为移动应用开发的重要平台。

本文将详细介绍如何使用Flutter框架开发一款跨平台的每日日记APP,并成功运行在鸿蒙系统上。我们将从项目架构设计、核心功能实现、鸿蒙平台适配等方面,全面展示Flutter跨平台开发的流程和最佳实践。

📱 应用介绍

功能概述

每日日记APP是一款简洁易用的跨平台日记应用,支持用户记录每日心情、天气和生活点滴。应用采用现代化的UI设计,提供流畅的用户体验,同时支持数据本地存储。

核心功能

  • ✍️ 日记管理:添加、编辑、删除日记
  • 📅 日期管理:按日期查看和管理日记
  • 😊 心情标签:支持选择多种心情标签(开心、平静、悲伤、愤怒等)
  • ☀️ 天气标签:支持选择多种天气标签(晴天、多云、雨天、雪天等)
  • 💾 本地存储:数据持久化存储,确保数据安全
  • 📱 跨平台兼容:同时支持鸿蒙、Android和iOS平台

技术栈

技术 版本/说明
框架 Flutter 3.x
语言 Dart 3.x
状态管理 原生StatefulWidget
存储 SharedPreferences + 内存存储(降级方案)
UI组件 Material Design
平台支持 鸿蒙、Android、iOS

🏗️ 项目架构设计

分层架构

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

├── models/          # 数据模型层,定义数据结构
├── services/        # 业务逻辑层,处理业务逻辑
├── screens/         # 界面展示层,显示UI和处理用户交互
└── widgets/         # 自定义组件层,复用UI组件

核心类关系图

DiaryEntry

+String id

+String title

+String content

+DateTime date

+String mood

+String weather

+toMap()

+fromMap()

StorageService

+Future init()

+Future getString()

+Future setString()

+Future remove()

DiaryService

+Future saveDiaryEntry()

+Future> getAllDiaryEntries()

+Future getDiaryEntryById()

+Future deleteDiaryEntry()

DiaryListScreen

+_loadDiaryEntries()

+_deleteDiaryEntry()

+_buildDiaryCard()

DiaryEditScreen

+_saveDiary()

+_generateId()

🚀 核心功能实现

1. 数据模型设计

/// 日记条目模型
/// 用于表示单个日记条目
class DiaryEntry {
  /// 日记ID
  String id;

  /// 日记标题
  String title;

  /// 日记内容
  String content;

  /// 日记日期
  DateTime date;

  /// 日记心情标签
  String mood;

  /// 日记天气标签
  String weather;

  /// 构造函数
  DiaryEntry({
    required this.id,
    required this.title,
    required this.content,
    required this.date,
    required this.mood,
    required this.weather,
  });

  /// 将日记条目转换为Map,用于存储
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'date': date.toIso8601String(),
      'mood': mood,
      'weather': weather,
    };
  }

  /// 从Map创建日记条目,用于读取存储数据
  factory DiaryEntry.fromMap(Map<String, dynamic> map) {
    return DiaryEntry(
      id: map['id'],
      title: map['title'],
      content: map['content'],
      date: DateTime.parse(map['date']),
      mood: map['mood'],
      weather: map['weather'],
    );
  }
}

2. 存储服务实现

/// 本地存储服务
class StorageService {
  /// 单例实例
  static final StorageService _instance = StorageService._internal();

  /// SharedPreferences实例
  SharedPreferences? _prefs;

  /// 内存存储,作为SharedPreferences的备选方案
  final Map<String, String> _memoryStorage = {};

  /// 构造函数
  factory StorageService() => _instance;

  /// 内部构造函数
  StorageService._internal();

  /// 初始化存储服务
  Future<void> init() async {
    try {
      _prefs = await SharedPreferences.getInstance();
      debugPrint('SharedPreferences初始化成功');
    } catch (e) {
      debugPrint('SharedPreferences初始化失败: $e');
      _prefs = null;
    }
  }

  /// 从存储中获取字符串值
  Future<String?> getString(String key, {String? defaultValue}) async {
    try {
      // 尝试从SharedPreferences获取
      if (_prefs != null) {
        try {
          final value = _prefs?.getString(key);
          if (value != null) {
            return value;
          }
        } catch (e) {
          debugPrint('从SharedPreferences获取字符串失败,使用内存存储: $e');
        }
      }

      // 从内存存储获取
      return _memoryStorage[key] ?? defaultValue;
    } catch (e) {
      debugPrint('获取字符串时出错: $e');
      return defaultValue;
    }
  }

  /// 保存字符串值到存储
  Future<void> setString(String key, String value) async {
    try {
      // 尝试保存到SharedPreferences
      if (_prefs != null) {
        try {
          await _prefs?.setString(key, value);
          debugPrint('保存字符串到SharedPreferences成功: $key');
          return;
        } catch (e) {
          debugPrint('保存字符串到SharedPreferences失败,使用内存存储: $e');
        }
      }

      // 保存到内存存储
      _memoryStorage[key] = value;
      debugPrint('保存字符串到内存存储成功: $key');
    } catch (e) {
      debugPrint('保存字符串时出错: $e');
    }
  }
}

3. 日记服务实现

/// 日记服务类
/// 用于处理日记的存储、读取、更新和删除操作
class DiaryService {
  /// 存储服务实例
  final StorageService _storageService = StorageService();

  /// 日记存储的键名
  static const String _diaryEntriesKey = 'diary_entries';

  /// 构造函数
  DiaryService();

  /// 保存日记条目
  Future<void> saveDiaryEntry(DiaryEntry entry) async {
    try {
      // 获取所有日记条目
      final entries = await getAllDiaryEntries();

      // 查找是否已存在相同ID的日记
      final index = entries.indexWhere((e) => e.id == entry.id);

      if (index != -1) {
        // 更新现有日记
        entries[index] = entry;
      } else {
        // 添加新日记
        entries.add(entry);
      }

      // 保存回存储
      final json = jsonEncode(entries.map((e) => e.toMap()).toList());
      await _storageService.setString(_diaryEntriesKey, json);

      debugPrint('日记保存成功: ${entry.title}');
    } catch (e) {
      debugPrint('保存日记时出错: $e');
    }
  }

  /// 获取所有日记条目
  Future<List<DiaryEntry>> getAllDiaryEntries() async {
    try {
      // 从存储中获取日记条目
      final json = await _storageService.getString(_diaryEntriesKey) ?? '[]';
      final List<dynamic> entriesJson = jsonDecode(json);

      // 转换为DiaryEntry对象并按日期倒序排序
      final entries = entriesJson
          .map((e) => DiaryEntry.fromMap(e as Map<String, dynamic>))
          .toList();

      // 按日期倒序排序
      entries.sort((a, b) => b.date.compareTo(a.date));

      return entries;
    } catch (e) {
      debugPrint('获取所有日记时出错: $e');
      return [];
    }
  }
}

4. 日记列表屏幕

/// 日记列表屏幕
class DiaryListScreen extends StatefulWidget {
  /// 构造函数
  const DiaryListScreen({super.key});

  
  State<DiaryListScreen> createState() => _DiaryListScreenState();
}

class _DiaryListScreenState extends State<DiaryListScreen> {
  /// 日记服务实例
  final DiaryService _diaryService = DiaryService();

  /// 日记列表
  List<DiaryEntry> _diaryEntries = [];

  /// 是否正在加载数据
  bool _isLoading = true;

  
  void initState() {
    super.initState();
    // 加载日记列表
    _loadDiaryEntries();
  }

  /// 加载日记列表
  Future<void> _loadDiaryEntries() async {
    setState(() {
      _isLoading = true;
    });

    final entries = await _diaryService.getAllDiaryEntries();

    setState(() {
      _diaryEntries = entries;
      _isLoading = false;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('每日日记'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),

      // 浮动操作按钮,用于添加新日记
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // 导航到编辑日记屏幕,添加新日记
          final result = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const DiaryEditScreen(),
            ),
          );

          // 如果返回true,重新加载日记列表
          if (result == true) {
            await _loadDiaryEntries();
          }
        },
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        child: const Icon(Icons.add),
      ),

      // 日记列表
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _diaryEntries.isEmpty
              ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(
                        Icons.book_outlined,
                        size: 64,
                        color: Colors.grey,
                      ),
                      SizedBox(height: 16),
                      Text(
                        '还没有日记',
                        style: TextStyle(fontSize: 18, color: Colors.grey),
                      ),
                      SizedBox(height: 8),
                      Text(
                        '点击右下角按钮添加第一篇日记吧',
                        style: TextStyle(fontSize: 14, color: Colors.grey),
                      ),
                    ],
                  ),
                )
              : ListView.builder(
                  padding: const EdgeInsets.all(8),
                  itemCount: _diaryEntries.length,
                  itemBuilder: (context, index) {
                    final entry = _diaryEntries[index];
                    return _buildDiaryCard(entry);
                  },
                ),
    );
  }

  /// 构建日记卡片
  Widget _buildDiaryCard(DiaryEntry entry) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8),
      elevation: 2,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: InkWell(
        onTap: () async {
          // 导航到编辑日记屏幕,查看/编辑日记
          final result = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => DiaryEditScreen(diaryEntry: entry),
            ),
          );

          // 如果返回true,重新加载日记列表
          if (result == true) {
            await _loadDiaryEntries();
          }
        },
        onLongPress: () => _deleteDiaryEntry(entry.id),
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 日记标题
              Text(
                entry.title,
                style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),

              const SizedBox(height: 8),

              // 日记内容预览
              Text(
                entry.content,
                style: const TextStyle(fontSize: 14, color: Colors.grey),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),

              const SizedBox(height: 12),

              // 日记元数据(日期、心情、天气)
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(
                    _formatDate(entry.date),
                    style: const TextStyle(fontSize: 12, color: Colors.grey),
                  ),

                  Row(
                    children: [
                      // 心情标签
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 8,
                          vertical: 4,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.blue.shade100,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          entry.mood,
                          style: const TextStyle(
                            fontSize: 12,
                            color: Colors.blue,
                          ),
                        ),
                      ),

                      const SizedBox(width: 8),

                      // 天气标签
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 8,
                          vertical: 4,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.yellow.shade100,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          entry.weather,
                          style: const TextStyle(
                            fontSize: 12,
                            color: Colors.orange,
                          ),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5. 日记编辑屏幕

/// 日记编辑屏幕
class DiaryEditScreen extends StatefulWidget {
  /// 可选的日记条目,用于编辑现有日记
  final DiaryEntry? diaryEntry;

  /// 构造函数
  const DiaryEditScreen({super.key, this.diaryEntry});

  
  State<DiaryEditScreen> createState() => _DiaryEditScreenState();
}

class _DiaryEditScreenState extends State<DiaryEditScreen> {
  /// 日记服务实例
  final DiaryService _diaryService = DiaryService();

  /// 表单键,用于表单验证
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  /// 标题控制器
  final TextEditingController _titleController = TextEditingController();

  /// 内容控制器
  final TextEditingController _contentController = TextEditingController();

  /// 当前选择的心情
  String _selectedMood = '开心';

  /// 当前选择的天气
  String _selectedWeather = '晴天';

  /// 可用的心情选项
  final List<String> _moodOptions = [
    '开心', '平静', '悲伤', '愤怒', '焦虑', '兴奋', '疲惫', '惊讶'
  ];

  /// 可用的天气选项
  final List<String> _weatherOptions = [
    '晴天', '多云', '雨天', '雪天', '阴天', '雾天', '大风'
  ];

  
  void initState() {
    super.initState();

    // 如果是编辑模式,初始化表单数据
    if (widget.diaryEntry != null) {
      _titleController.text = widget.diaryEntry!.title;
      _contentController.text = widget.diaryEntry!.content;
      _selectedMood = widget.diaryEntry!.mood;
      _selectedWeather = widget.diaryEntry!.weather;
    }
  }

  
  void dispose() {
    // 释放控制器资源
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  /// 保存日记
  Future<void> _saveDiary() async {
    // 验证表单
    if (!_formKey.currentState!.validate()) {
      return;
    }

    // 创建日记条目
    final entry = DiaryEntry(
      id: widget.diaryEntry?.id ?? _generateId(),
      title: _titleController.text.trim(),
      content: _contentController.text.trim(),
      date: widget.diaryEntry?.date ?? DateTime.now(),
      mood: _selectedMood,
      weather: _selectedWeather,
    );

    // 保存日记
    await _diaryService.saveDiaryEntry(entry);

    // 返回上一个页面,并传递成功信号
    Navigator.pop(context, true);
  }

  /// 生成唯一ID
  String _generateId() {
    // 使用时间戳和随机数生成唯一ID
    return DateTime.now().millisecondsSinceEpoch.toString() +
           Random().nextInt(1000).toString();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.diaryEntry == null ? '添加日记' : '编辑日记'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        actions: [
          // 保存按钮
          TextButton(
            onPressed: _saveDiary,
            child: const Text(
              '保存',
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
          ),
        ],
      ),

      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 日记标题输入
              TextFormField(
                controller: _titleController,
                decoration: const InputDecoration(
                  labelText: '日记标题',
                  hintText: '请输入日记标题',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return '请输入日记标题';
                  }
                  return null;
                },
                style: const TextStyle(fontSize: 18),
              ),

              const SizedBox(height: 16),

              // 日记内容输入
              TextFormField(
                controller: _contentController,
                decoration: const InputDecoration(
                  labelText: '日记内容',
                  hintText: '今天发生了什么有趣的事情?',
                  border: OutlineInputBorder(),
                  alignLabelWithHint: true,
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return '请输入日记内容';
                  }
                  return null;
                },
                maxLines: 10,
                style: const TextStyle(fontSize: 16),
                keyboardType: TextInputType.multiline,
              ),

              const SizedBox(height: 20),

              // 心情选择
              const Text(
                '心情',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),

              const SizedBox(height: 8),

              Wrap(
                spacing: 8,
                runSpacing: 8,
                children: _moodOptions.map((mood) {
                  return ChoiceChip(
                    label: Text(mood),
                    selected: _selectedMood == mood,
                    onSelected: (selected) {
                      if (selected) {
                        setState(() {
                          _selectedMood = mood;
                        });
                      }
                    },
                    selectedColor: Colors.blue,
                    labelStyle: TextStyle(
                      color: _selectedMood == mood ?

Colors.white : Colors.black,
),
);
}).toList(),
),

          const SizedBox(height: 20),
          
          // 天气选择
          const Text(
            '天气',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          
          const SizedBox(height: 8),
          
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: _weatherOptions.map((weather) {
              return ChoiceChip(
                label: Text(weather),
                selected: _selectedWeather == weather,
                onSelected: (selected) {
                  if (selected) {
                    setState(() {
                      _selectedWeather = weather;
                    });
                  }
                },
                selectedColor: Colors.orange,
                labelStyle: TextStyle(
                  color: _selectedWeather == weather ? Colors.white : Colors.black,
                ),
              );
            }).toList(),
          ),
          
          const SizedBox(height: 32),
          
          // 保存按钮
          SizedBox(
            width: double.infinity,
            height: 50,
            child: ElevatedButton(
              onPressed: _saveDiary,
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                foregroundColor: Colors.white,
                textStyle: const TextStyle(fontSize: 18),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(8),
                ),
              ),
              child: const Text('保存日记'),
            ),
          ),
        ],
      ),
    ),
  ),
);

🛠️ 鸿蒙平台适配

配置调整

  1. 添加鸿蒙支持:在项目根目录下运行flutter create --platforms ohos .,添加鸿蒙平台支持

  2. 修改main.dart:更新入口文件,初始化存储服务

/// 日记APP主入口
void main() async {
  // 确保Flutter初始化完成
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化存储服务
  final storageService = StorageService();
  await storageService.init();
  
  runApp(const DiaryApp());
}

平台兼容性处理

在鸿蒙平台上,部分Flutter插件可能存在兼容性问题,如SharedPreferences。为了解决这个问题,我们实现了以下方案:

  1. 双层存储机制

    • 优先使用SharedPreferences进行持久化存储
    • 当SharedPreferences不可用时,自动切换到内存存储
    • 确保应用在任何情况下都能正常运行
  2. 优雅的错误处理

    • 捕获并记录平台特定的异常
    • 提供友好的用户反馈
    • 确保应用不会崩溃
  3. 条件编译优化

    • 针对不同平台使用不同的实现
    • 优化性能和资源占用

适配流程图

成功

失败

保存日记

SharedPreferences可用

SharedPreferences不可用

应用启动

初始化SharedPreferences

使用SharedPreferences存储

使用内存存储

正常运行

用户操作

调用存储服务

保存到SharedPreferences

保存到内存

操作完成

🚀 项目构建与运行

构建流程

开发代码

代码检查

构建HAP包

签名HAP包

安装到设备

运行应用

运行命令

# 运行应用
flutter run

# 仅构建HAP包
flutter build ohos

# 构建并安装HAP包
flutter install

调试工具

  • Flutter DevTools:用于调试UI、性能分析和内存监控
  • 鸿蒙开发者工具:用于鸿蒙平台特定的调试和分析
  • Dart Observatory:用于Dart VM的调试和性能分析

📊 性能优化

内存优化

  1. 合理使用控制器:及时释放TextEditingController等资源
  2. 避免内存泄漏:正确管理Stream和Subscription
  3. 优化列表渲染:使用ListView.builder进行懒加载
  4. 减少不必要的重建:合理使用const和final关键字

性能监控

  1. 使用Flutter DevTools:监控应用性能指标
  2. 添加性能日志:记录关键操作的执行时间
  3. 分析渲染性能:使用Performance Overlay查看渲染帧率
  4. 优化布局层次:减少Widget树的深度

🎯 总结

技术经验

  1. 分层架构设计:采用分层架构,实现了UI层、业务层和数据层的解耦,提高了代码的可维护性和可扩展性

  2. 平台兼容性处理:针对鸿蒙平台的特性,实现了双层存储机制和优雅的错误处理,确保应用在不同平台上都能正常运行

  3. 性能优化:通过合理使用控制器、优化列表渲染等方式,提高了应用的性能和响应速度

  4. 用户体验设计:采用现代化的UI设计和流畅的交互体验,提高了用户的使用满意度

未来展望

🔮 功能扩展

  • 添加云同步功能,实现多设备数据同步
  • 支持图片和视频附件,丰富日记内容
  • 添加日记分类和搜索功能,方便用户管理
  • 支持日记导出和导入,增强数据安全性
  • 添加主题切换功能,满足个性化需求

📝 结语

通过本项目的开发,我们成功实现了一款基于Flutter框架的跨平台日记APP,并在鸿蒙系统上运行。Flutter的跨平台特性使得我们能够用一套代码同时支持多个平台,大大提高了开发效率。同时,我们也遇到了一些平台特定的挑战,但通过合理的设计和优化,成功解决了这些问题。

随着Flutter框架的不断发展和鸿蒙系统的日益成熟,跨平台开发将变得更加便捷和高效。我们期待未来能够看到更多基于Flutter的优秀跨平台应用出现在鸿蒙生态中。

💡 如果你对本项目有任何疑问或建议,欢迎留言交流!


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐