Flutter 鸿蒙:基于第三方库的日记本应用
本文档将指导你从零开始创建一个完整的 Flutter 鸿蒙日记本应用。
·
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
本文档将指导你从零开始创建一个完整的Flutter鸿蒙日记本应用。
📋 目录
创建项目
1. 创建 Flutter 项目
flutter create --platforms ohos diary_app
cd diary_app
flutter build app --release
2. 添加依赖
编辑 pubspec.yaml:
name: diary_app
description: 鸿蒙日记本应用
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# 国际化
intl: ^0.18.0
# 文件存储(纯 Dart 实现,兼容鸿蒙)
path: ^1.8.3
# UI 动画
flutter_staggered_animations: ^1.1.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
安装依赖:
flutter pub get
项目结构
lib/
├── main.dart # 应用入口
├── models/
│ └── diary_model.dart # 日记数据模型
├── screens/
│ ├── home_screen.dart # 主页(日记列表)
│ ├── diary_editor_screen.dart # 日记编辑页面
│ ├── diary_detail_screen.dart # 日记详情页面
│ └── stats_screen.dart # 统计信息页面
└── services/
├── shared_preferences_storage_service.dart # 文件存储服务
├── database_helper.dart # 数据库操作帮助类
└── image_storage_service.dart # 图片存储服务
核心功能实现
1. 数据模型 (diary_model.dart)
import 'package:flutter/material.dart';
/// 日记数据模型
class Diary {
final int? id;
final String title;
final String content;
final String mood;
final String weather;
final String? imagePath;
final DateTime createdAt;
final DateTime updatedAt;
Diary({
this.id,
required this.title,
required this.content,
required this.mood,
required this.weather,
this.imagePath,
required this.createdAt,
required this.updatedAt,
});
/// 从 Map 对象转换为 Diary 对象
factory Diary.fromMap(Map<String, dynamic> map) {
return Diary(
id: map['id'] as int?,
title: map['title'] as String,
content: map['content'] as String,
mood: map['mood'] as String,
weather: map['weather'] as String,
imagePath: map['imagePath'] as String?,
createdAt: DateTime.parse(map['createdAt'] as String),
updatedAt: DateTime.parse(map['updatedAt'] as String),
);
}
/// 转换为 Map 对象
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'content': content,
'mood': mood,
'weather': weather,
'imagePath': imagePath,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
Diary copyWith({
int? id,
String? title,
String? content,
String? mood,
String? weather,
String? imagePath,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Diary(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
mood: mood ?? this.mood,
weather: weather ?? this.weather,
imagePath: imagePath ?? this.imagePath,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
/// 心情选项
class MoodOption {
final String value;
final String display;
final IconData icon;
const MoodOption({
required this.value,
required this.display,
required this.icon,
});
static const List<MoodOption> options = [
MoodOption(value: 'happy', display: '开心', icon: Icons.sentiment_very_satisfied),
MoodOption(value: 'calm', display: '平静', icon: Icons.sentiment_satisfied),
MoodOption(value: 'sad', display: '悲伤', icon: Icons.sentiment_dissatisfied),
MoodOption(value: 'excited', display: '兴奋', icon: Icons.sentiment_very_satisfied),
];
static MoodOption fromValue(String value) {
return options.firstWhere(
(option) => option.value == value,
orElse: () => options.first,
);
}
}
/// 天气选项
class WeatherOption {
final String value;
final String display;
final IconData icon;
const WeatherOption({
required this.value,
required this.display,
required this.icon,
});
static const List<WeatherOption> options = [
WeatherOption(value: 'sunny', display: '晴', icon: Icons.wb_sunny),
WeatherOption(value: 'cloudy', display: '多云', icon: Icons.cloud),
WeatherOption(value: 'rainy', display: '雨', icon: Icons.umbrella),
WeatherOption(value: 'snowy', display: '雪', icon: Icons.ac_unit),
];
static WeatherOption fromValue(String value) {
return options.firstWhere(
(option) => option.value == value,
orElse: () => options.first,
);
}
}
2. 文件存储服务 (shared_preferences_storage_service.dart)
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
/// 基于文件的日记存储服务
/// 纯 Dart 实现,完美兼容鸿蒙平台
class FileStorageService {
static final FileStorageService _instance = FileStorageService._internal();
factory FileStorageService() => _instance;
FileStorageService._internal();
static const String _storageFileName = 'diaries.json';
final Map<String, Map<String, dynamic>> _diaryCache = {};
File? _storageFile;
/// 初始化存储服务
Future<void> init() async {
try {
final appDir = await _getApplicationDataDirectory();
_storageFile = File(p.join(appDir.path, _storageFileName));
if (!await appDir.exists()) {
await appDir.create(recursive: true);
}
await _loadFromFile();
} catch (e) {
rethrow;
}
}
/// 获取应用数据目录
Future<Directory> _getApplicationDataDirectory() async {
String dataPath = Directory.systemTemp.path;
dataPath = p.join(dataPath, 'diary_app');
return Directory(dataPath);
}
/// 从文件加载数据
Future<void> _loadFromFile() async {
if (_storageFile != null && await _storageFile!.exists()) {
try {
final jsonString = await _storageFile!.readAsString();
if (jsonString.isNotEmpty) {
final Map<String, dynamic> data = jsonDecode(jsonString) as Map<String, dynamic>;
_diaryCache.clear();
data.forEach((key, value) {
_diaryCache[key] = Map<String, dynamic>.from(value as Map);
});
}
} catch (e) {
await _saveToFile();
}
} else {
await _saveToFile();
}
}
/// 保存数据到文件
Future<void> _saveToFile() async {
if (_storageFile != null) {
final jsonString = jsonEncode(_diaryCache);
await _storageFile!.writeAsString(jsonString);
}
}
/// 保存日记
Future<void> saveDiary(String key, Map<String, dynamic> data) async {
_diaryCache[key] = data;
await _saveToFile();
}
/// 获取日记
Future<Map<String, dynamic>?> getDiary(String key) async {
return _diaryCache[key];
}
/// 获取所有日记
Future<List<Map<String, dynamic>>> getAllDiaries() async {
await _loadFromFile(); // 关键:每次获取前都从文件重新加载
final allData = _diaryCache.values.toList();
allData.sort((a, b) {
final aTime = DateTime.parse(a['createdAt'] as String);
final bTime = DateTime.parse(b['createdAt'] as String);
return bTime.compareTo(aTime);
});
return allData;
}
/// 删除日记
Future<void> deleteDiary(String key) async {
_diaryCache.remove(key);
await _saveToFile();
}
/// 获取日记总数
Future<int> getDiaryCount() async {
return _diaryCache.length;
}
/// 获取连续写作天数
Future<int> getContinuousWritingDays() async {
final allDiaries = await getAllDiaries();
if (allDiaries.isEmpty) return 0;
final dates = allDiaries
.map((d) => DateTime.parse(d['createdAt'] as String))
.map((dt) => DateTime(dt.year, dt.month, dt.day))
.toSet()
.toList();
dates.sort((a, b) => b.compareTo(a));
if (dates.isEmpty) return 0;
int continuousDays = 1;
final today = DateTime.now();
final yesterday = DateTime(today.year, today.month, today.day - 1);
final latestDate = dates.first;
if (latestDate.isBefore(yesterday) && latestDate.isBefore(today)) {
return 0;
}
for (int i = 0; i < dates.length - 1; i++) {
final diff = dates[i].difference(dates[i + 1]).inDays;
if (diff == 1) {
continuousDays++;
} else {
break;
}
}
return continuousDays;
}
/// 搜索日记
Future<List<Map<String, dynamic>>> searchDiaries(String keyword) async {
if (keyword.trim().isEmpty) {
return getAllDiaries();
}
final allDiaries = await getAllDiaries();
final keywordLower = keyword.toLowerCase();
return allDiaries.where((diary) {
final title = (diary['title'] as String).toLowerCase();
final content = (diary['content'] as String).toLowerCase();
return title.contains(keywordLower) || content.contains(keywordLower);
}).toList();
}
}
3. 数据库帮助类 (database_helper.dart)
import '../models/diary_model.dart';
import 'shared_preferences_storage_service.dart';
/// 数据库操作帮助类
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
factory DatabaseHelper() => _instance;
DatabaseHelper._internal();
late final FileStorageService _storage = FileStorageService();
/// 初始化数据库
Future<void> init() async {
await _storage.init();
}
/// 保存日记
Future<int> saveDiary(Diary diary) async {
final now = DateTime.now();
final diaryMap = {
'id': diary.id ?? now.millisecondsSinceEpoch ~/ 1000,
'title': diary.title,
'content': diary.content,
'mood': diary.mood,
'weather': diary.weather,
'imagePath': diary.imagePath,
'createdAt': diary.createdAt.toIso8601String(),
'updatedAt': diary.updatedAt.toIso8601String(),
};
final key = diaryMap['id'].toString();
await _storage.saveDiary(key, diaryMap);
return diaryMap['id'] as int;
}
/// 获取所有日记
Future<List<Diary>> getAllDiaries() async {
final diaries = await _storage.getAllDiaries();
return diaries.map((map) => Diary.fromMap(map)).toList();
}
/// 获取日记详情
Future<Diary?> getDiaryById(int id) async {
final diaryMap = await _storage.getDiary(id.toString());
if (diaryMap == null) return null;
return Diary.fromMap(diaryMap);
}
/// 删除日记
Future<void> deleteDiary(int id) async {
await _storage.deleteDiary(id.toString());
}
/// 批量删除日记
Future<void> deleteDiaries(List<int> ids) async {
for (final id in ids) {
await _storage.deleteDiary(id.toString());
}
}
/// 更新日记
Future<void> updateDiary(Diary diary) async {
await saveDiary(diary);
}
/// 插入日记
Future<int> insertDiary(Diary diary) async {
return await saveDiary(diary);
}
/// 获取日记总数
Future<int> getDiaryCount() async {
return await _storage.getDiaryCount();
}
/// 获取连续写作天数
Future<int> getContinuousWritingDays() async {
return await _storage.getContinuousWritingDays();
}
/// 搜索日记
Future<List<Diary>> searchDiaries(String keyword) async {
final diaries = await _storage.searchDiaries(keyword);
return diaries.map((map) => Diary.fromMap(map)).toList();
}
}
4. 应用入口 (main.dart)
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'screens/home_screen.dart';
import 'services/database_helper.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化日期本地化
await initializeDateFormatting('zh_CN', null);
// 初始化数据库
final dbHelper = DatabaseHelper();
await dbHelper.init();
runApp(const DiaryApp());
}
class DiaryApp extends StatelessWidget {
const DiaryApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '我的日记',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
5. 主页 (home_screen.dart)
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import '../services/database_helper.dart';
import '../models/diary_model.dart';
import 'diary_editor_screen.dart';
import 'diary_detail_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final DatabaseHelper _dbHelper = DatabaseHelper();
List<Diary> _diaries = [];
bool _isLoading = true;
void initState() {
super.initState();
_loadData();
}
/// 加载所有数据
Future<void> _loadData() async {
try {
setState(() => _isLoading = true);
final diaries = await _dbHelper.getAllDiaries();
final totalCount = await _dbHelper.getDiaryCount();
final continuousDays = await _dbHelper.getContinuousWritingDays();
setState(() {
_diaries = diaries;
_isLoading = false;
});
} catch (e) {
setState(() {
_diaries = [];
_isLoading = false;
});
}
}
/// 打开编辑器
Future<void> _openEditor([Diary? diary]) async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DiaryEditorScreen(diary: diary),
),
);
if (result == true) {
await _loadData();
}
}
/// 打开详情
Future<void> _openDetail(Diary diary) async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DiaryDetailScreen(diary: diary),
),
);
if (result == true) {
await _loadData();
}
}
/// 显示统计信息
void _showStatsDialog() async {
final totalCount = await _dbHelper.getDiaryCount();
final continuousDays = await _dbHelper.getContinuousWritingDays();
if (!mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('统计信息'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.description),
title: const Text('日记总数'),
trailing: Text('$totalCount 篇'),
),
ListTile(
leading: const Icon(Icons.calendar_today),
title: const Text('连续写作'),
trailing: Text('$continuousDays 天'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
],
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('我的日记'),
actions: [
IconButton(
icon: const Icon(Icons.bar_chart),
onPressed: _showStatsDialog,
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _diaries.isEmpty
? _buildEmptyState()
: _buildDiaryList(),
floatingActionButton: FloatingActionButton(
onPressed: () => _openEditor(),
child: const Icon(Icons.add),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.auto_awesome,
size: 80,
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
'还没有任何日记',
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'点击右下角的按钮开始记录',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
);
}
Widget _buildDiaryList() {
return AnimationLimiter(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _diaries.length,
itemBuilder: (context, index) {
final diary = _diaries[index];
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 375),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: Icon(
MoodOption.fromValue(diary.mood).icon,
color: Theme.of(context).colorScheme.primary,
),
title: Text(diary.title),
subtitle: Text(
diary.content,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
'${diary.createdAt.month}/${diary.createdAt.day}',
style: TextStyle(color: Colors.grey.shade600),
),
onTap: () => _openDetail(diary),
),
),
),
),
);
},
),
);
}
}
6. 日记编辑器 (diary_editor_screen.dart)
import 'package:flutter/material.dart';
import '../models/diary_model.dart';
import '../services/database_helper.dart';
class DiaryEditorScreen extends StatefulWidget {
final Diary? diary;
const DiaryEditorScreen({super.key, this.diary});
State<DiaryEditorScreen> createState() => _DiaryEditorScreenState();
}
class _DiaryEditorScreenState extends State<DiaryEditorScreen> {
final DatabaseHelper _dbHelper = DatabaseHelper();
final TextEditingController _titleController = TextEditingController();
final TextEditingController _contentController = TextEditingController();
String _selectedMood = 'happy';
String _selectedWeather = 'sunny';
bool _isSaving = false;
bool _isEditing = false;
void initState() {
super.initState();
_isEditing = widget.diary != null;
if (_isEditing) {
_titleController.text = widget.diary!.title;
_contentController.text = widget.diary!.content;
_selectedMood = widget.diary!.mood;
_selectedWeather = widget.diary!.weather;
}
}
void dispose() {
_titleController.dispose();
_contentController.dispose();
super.dispose();
}
Future<void> _saveDiary() async {
if (_titleController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入标题')),
);
return;
}
setState(() => _isSaving = true);
try {
final now = DateTime.now();
if (_isEditing) {
final updatedDiary = widget.diary!.copyWith(
title: _titleController.text.trim(),
content: _contentController.text.trim(),
mood: _selectedMood,
weather: _selectedWeather,
updatedAt: now,
);
await _dbHelper.updateDiary(updatedDiary);
} else {
final newDiary = Diary(
title: _titleController.text.trim(),
content: _contentController.text.trim(),
mood: _selectedMood,
weather: _selectedWeather,
createdAt: now,
updatedAt: now,
);
await _dbHelper.insertDiary(newDiary);
}
if (mounted) {
Navigator.pop(context, true);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存失败:$e')),
);
setState(() => _isSaving = false);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isEditing ? '编辑日记' : '写日记'),
actions: [
IconButton(
icon: const Icon(Icons.check),
onPressed: _isSaving ? null : _saveDiary,
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题',
border: OutlineInputBorder(),
),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
children: [
const Text('心情:'),
...MoodOption.options.map((option) => Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(option.display),
selected: _selectedMood == option.value,
onSelected: (selected) {
if (selected) {
setState(() => _selectedMood = option.value);
}
},
),
)),
],
),
const SizedBox(height: 16),
Row(
children: [
const Text('天气:'),
...WeatherOption.options.map((option) => Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(option.display),
selected: _selectedWeather == option.value,
onSelected: (selected) {
if (selected) {
setState(() => _selectedWeather = option.value);
}
},
),
)),
],
),
const SizedBox(height: 16),
TextField(
controller: _contentController,
decoration: const InputDecoration(
labelText: '内容',
border: OutlineInputBorder(),
),
maxLines: 10,
),
],
),
),
);
}
}
7. 日记详情 (diary_detail_screen.dart)
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/diary_model.dart';
import 'diary_editor_screen.dart';
class DiaryDetailScreen extends StatelessWidget {
final Diary diary;
const DiaryDetailScreen({super.key, required this.diary});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(diary.title),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DiaryEditorScreen(diary: diary),
),
);
if (result == true && context.mounted) {
Navigator.pop(context, true);
}
},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
MoodOption.fromValue(diary.mood).icon,
size: 32,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Icon(
WeatherOption.fromValue(diary.weather).icon,
size: 24,
color: Colors.grey.shade600,
),
const Spacer(),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(diary.createdAt),
style: TextStyle(color: Colors.grey.shade600),
),
],
),
const Divider(height: 32),
Text(
diary.content,
style: const TextStyle(fontSize: 16, height: 1.6),
),
],
),
),
);
}
}
鸿蒙平台适配
关键要点
-
使用纯 Dart 实现存储
- 避免使用
path_provider、shared_preferences等依赖平台通道的插件 - 使用
dart:io的File和Directory进行文件操作
- 避免使用
-
单例模式确保数据一致性
FileStorageService使用单例模式- 每次读取数据前从文件重新加载
-
字段命名一致性
- 保存和读取使用相同的字段命名
常见问题
问题 1:MissingPluginException
错误信息:
MissingPluginException(No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider)
解决方案:
使用纯 Dart 的 Directory.systemTemp 代替:
String dataPath = Directory.systemTemp.path;
dataPath = p.join(dataPath, 'diary_app');
问题 2:数据保存后主页不更新
原因:缓存没有从文件重新加载
解决方案:
在 getAllDiaries() 中每次获取前都从文件重新加载:
Future<List<Map<String, dynamic>>> getAllDiaries() async {
await _loadFromFile(); // 关键修复
final allData = _diaryCache.values.toList();
// ...
}
问题 3:权限错误
错误信息:
PathAccessException: Creation failed, path = 'xxx' (OS Error: Permission denied, errno = 13)
解决方案:
使用系统临时目录,不需要额外权限:
String dataPath = Directory.systemTemp.path;
运行项目
1. 连接设备或启动模拟器
在 DevEco Studio 中:
- 连接真机(开发者模式 + USB 调试)
- 或启动 OpenHarmony 模拟器
2. 运行应用
# 方式 1:使用 Flutter 命令
flutter run -d <device-id>
# 方式 2:在 DevEco Studio 中点击运行按钮
3. 查看日志
# 查看 Flutter 日志
flutter logs
# 或在 DevEco Studio 的 Log 窗口查看
项目优化建议
1. 性能优化
- 使用
ListView.builder懒加载长列表 - 图片压缩和缓存
- 数据库分页加载
2. 功能增强
- 日记分类/标签
- 日记导出(PDF、图片)
- 云同步备份
- 密码保护
3. UI/UX 改进
- 主题切换(深色模式)
- 日记封面图片
- 心情/天气统计图表
- 日历视图
成果展示



总结
本项目展示了如何在鸿蒙平台上开发纯 Dart 实现的 Flutter 应用,关键点是:
- ✅ 避免平台依赖 - 使用纯 Dart IO 进行文件存储
- ✅ 数据一致性 - 单例模式 + 文件重新加载
- ✅ 字段一致性 - 保存和读取使用相同字段名
- ✅ 错误处理 - 完善的异常捕获和日志输出
通过以上步骤,你可以成功复现一个完整的鸿蒙日记本应用!🎉
更多推荐


所有评论(0)