1. 项目概述:手账便签纸收藏应用开发
作为一名手账爱好者,我经常遇到便签纸管理混乱的问题——不同品牌、系列、颜色的便签纸散落在各处,使用时经常找不到想要的款式。为了解决这个痛点,我决定开发一款专门用于管理便签纸收藏的Flutter应用。
这款应用的核心功能包括:
- 便签纸档案管理:记录每款便签纸的品牌、系列、尺寸、材质等详细信息
- 智能分类系统:支持按品牌、颜色、用途等多维度分类
- 使用追踪:记录每款便签纸的使用情况,避免"买了不用"的浪费
- 精美展示:网格和列表两种视图展示收藏品
- 数据分析:统计收藏价值、使用率等关键指标
应用采用Flutter 3.x框架开发,适配鸿蒙系统(HarmonyOS)和Android/iOS平台。下面我将详细介绍开发过程中的关键技术和实现细节。
2. 技术选型与架构设计
2.1 技术栈选择
选择Flutter作为开发框架主要基于以下考虑:
- 跨平台能力:一套代码可同时运行在鸿蒙、Android和iOS平台
- 高性能渲染:Skia图形引擎保证UI流畅度
- 丰富的组件库:Material Design 3组件开箱即用
- 热重载:大幅提升开发效率
dart复制// 主应用入口配置
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '手账便签纸收藏',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
useMaterial3: true,
),
home: const StickyNoteHomePage(),
);
}
}
2.2 数据模型设计
应用的核心数据模型包括三个主要部分:
2.2.1 便签纸模型(StickyNote)
dart复制class StickyNote {
final String id; // 唯一标识
final String name; // 便签纸名称
final String brand; // 品牌
final String series; // 系列
final String size; // 尺寸
final String color; // 颜色
final String material; // 材质
final String pattern; // 图案/花纹
final double price; // 价格
final DateTime purchaseDate; // 购买日期
final String purchasePlace; // 购买地点
final int totalSheets; // 总张数
final int usedSheets; // 已使用张数
final List<String> tags; // 标签
final String notes; // 备注
final List<String> photos; // 照片
final String condition; // 保存状态
bool isFavorite; // 是否收藏
double rating; // 评分
}
2.2.2 使用记录模型(UsageRecord)
dart复制class UsageRecord {
final String id;
final String stickyNoteId;
final DateTime usageDate;
final int sheetsUsed;
final String purpose;
final String project;
final String notes;
final List<String> photos;
}
2.2.3 分类枚举
dart复制enum NoteCategory {
memo, // 便签
decoration, // 装饰
index, // 索引
bookmark, // 书签
label, // 标签
special, // 特殊用途
}
2.3 应用架构
应用采用典型的MVVM架构:
- Model:数据模型层(StickyNote、UsageRecord等)
- View:UI展示层(Widget树)
- ViewModel:业务逻辑层(状态管理)
dart复制// 状态管理示例
class StickyNoteViewModel extends ChangeNotifier {
List<StickyNote> _stickyNotes = [];
List<StickyNote> get stickyNotes => _stickyNotes;
void addStickyNote(StickyNote note) {
_stickyNotes.add(note);
notifyListeners();
}
void updateStickyNote(StickyNote note) {
final index = _stickyNotes.indexWhere((n) => n.id == note.id);
if (index != -1) {
_stickyNotes[index] = note;
notifyListeners();
}
}
}
3. 核心功能实现
3.1 便签纸展示页面
3.1.1 网格视图实现
dart复制GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _filteredNotes.length,
itemBuilder: (context, index) => _buildStickyNoteCard(_filteredNotes[index]),
)
3.1.2 列表视图实现
dart复制ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _filteredNotes.length,
itemBuilder: (context, index) => _buildStickyNoteListItem(_filteredNotes[index]),
)
3.1.3 便签纸卡片组件
dart复制Widget _buildStickyNoteCard(StickyNote note) {
final remainingSheets = note.totalSheets - note.usedSheets;
final usagePercentage = note.totalSheets > 0
? note.usedSheets / note.totalSheets
: 0.0;
return Card(
elevation: 4,
margin: const EdgeInsets.all(8),
child: InkWell(
onTap: () => _showNoteDetail(note),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图片区域
Container(
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getColorFromName(note.color),
_getColorFromName(note.color).withOpacity(0.7),
],
),
),
child: Stack(
children: [
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
),
child: Text(note.brand, style: const TextStyle(fontSize: 10)),
),
),
],
),
),
// 信息区域
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(note.name, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('${note.series} • ${note.size}', style: TextStyle(color: Colors.grey.shade600)),
const SizedBox(height: 8),
// 使用进度条
LinearProgressIndicator(
value: usagePercentage,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation<Color>(
usagePercentage > 0.8 ? Colors.red : Colors.green,
),
),
],
),
),
],
),
),
);
}
3.2 分类管理功能
3.2.1 品牌分类实现
dart复制Widget _buildBrandCategories() {
final brandStats = <String, int>{};
for (final note in _stickyNotes) {
brandStats[note.brand] = (brandStats[note.brand] ?? 0) + 1;
}
return Wrap(
spacing: 12,
runSpacing: 12,
children: brandStats.entries.map((entry) {
return InkWell(
onTap: () => _filterByBrand(entry.key),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: _getBrandColor(entry.key).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _getBrandColor(entry.key)),
),
child: Column(
children: [
Icon(_getBrandIcon(entry.key), color: _getBrandColor(entry.key)),
const SizedBox(height: 4),
Text(entry.key),
Text('${entry.value}款'),
],
),
),
);
}).toList(),
);
}
3.2.2 颜色分类实现
dart复制Widget _buildColorCategories() {
final colorStats = <String, int>{};
for (final note in _stickyNotes) {
colorStats[note.color] = (colorStats[note.color] ?? 0) + 1;
}
return Wrap(
spacing: 12,
runSpacing: 12,
children: colorStats.entries.map((entry) {
return InkWell(
onTap: () => _filterByColor(entry.key),
child: Container(
width: 80,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _getColorFromName(entry.key).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _getColorFromName(entry.key)),
),
child: Column(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: _getColorFromName(entry.key),
shape: BoxShape.circle,
),
),
const SizedBox(height: 8),
Text(entry.key),
Text('${entry.value}款', style: const TextStyle(fontSize: 10)),
],
),
),
);
}).toList(),
);
}
3.3 使用记录功能
3.3.1 记录使用对话框
dart复制void _showUsageDialog(StickyNote note) {
final sheetsController = TextEditingController();
final purposeController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('记录使用 - ${note.name}'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: sheetsController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: '使用张数'),
),
const SizedBox(height: 16),
TextField(
controller: purposeController,
decoration: const InputDecoration(labelText: '使用用途'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () {
final sheets = int.tryParse(sheetsController.text) ?? 0;
if (sheets > 0 && sheets <= (note.totalSheets - note.usedSheets)) {
_addUsageRecord(note, sheets, purposeController.text);
Navigator.pop(context);
}
},
child: const Text('确认'),
),
],
),
);
}
3.3.2 添加使用记录
dart复制void _addUsageRecord(StickyNote note, int sheetsUsed, String purpose) {
final record = UsageRecord(
id: DateTime.now().millisecondsSinceEpoch.toString(),
stickyNoteId: note.id,
usageDate: DateTime.now(),
sheetsUsed: sheetsUsed,
purpose: purpose,
);
setState(() {
_usageRecords.add(record);
final index = _stickyNotes.indexWhere((n) => n.id == note.id);
if (index != -1) {
_stickyNotes[index] = StickyNote(
...note,
usedSheets: note.usedSheets + sheetsUsed,
);
}
});
}
3.4 统计分析功能
3.4.1 统计数据计算
dart复制class CollectionStats {
final int totalNotes;
final int totalSheets;
final int usedSheets;
final double totalValue;
final double averageRating;
final Map<String, int> brandDistribution;
final Map<String, int> colorDistribution;
final double usageRate;
CollectionStats({
required this.totalNotes,
required this.totalSheets,
required this.usedSheets,
required this.totalValue,
required this.averageRating,
required this.brandDistribution,
required this.colorDistribution,
required this.usageRate,
});
}
void _calculateStats() {
final totalNotes = _stickyNotes.length;
final totalSheets = _stickyNotes.fold<int>(0, (sum, note) => sum + note.totalSheets);
final usedSheets = _stickyNotes.fold<int>(0, (sum, note) => sum + note.usedSheets);
final totalValue = _stickyNotes.fold<double>(0, (sum, note) => sum + note.price);
final ratings = _stickyNotes.where((note) => note.rating > 0);
final averageRating = ratings.isNotEmpty
? ratings.fold<double>(0, (sum, note) => sum + note.rating) / ratings.length
: 0.0;
final brandDistribution = <String, int>{};
final colorDistribution = <String, int>{};
for (final note in _stickyNotes) {
brandDistribution[note.brand] = (brandDistribution[note.brand] ?? 0) + 1;
colorDistribution[note.color] = (colorDistribution[note.color] ?? 0) + 1;
}
final usageRate = totalSheets > 0 ? usedSheets / totalSheets : 0.0;
_stats = CollectionStats(
totalNotes: totalNotes,
totalSheets: totalSheets,
usedSheets: usedSheets,
totalValue: totalValue,
averageRating: averageRating,
brandDistribution: brandDistribution,
colorDistribution: colorDistribution,
usageRate: usageRate,
);
}
3.4.2 统计页面展示
dart复制Widget _buildStatsPage() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const Text('收藏统计', style: TextStyle(fontSize: 20)),
const SizedBox(height: 20),
Row(
children: [
_buildStatItem('总收藏', '${_stats.totalNotes}款', Icons.collections),
_buildStatItem('总张数', '${_stats.totalSheets}张', Icons.layers),
],
),
const SizedBox(height: 16),
Row(
children: [
_buildStatItem('总价值', '¥${_stats.totalValue.toStringAsFixed(0)}', Icons.attach_money),
_buildStatItem('平均评分', '${_stats.averageRating.toStringAsFixed(1)}分', Icons.star),
],
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('品牌分布', style: TextStyle(fontSize: 18)),
const SizedBox(height: 16),
..._stats.brandDistribution.entries.map((entry) =>
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Icon(_getBrandIcon(entry.key), color: _getBrandColor(entry.key)),
const SizedBox(width: 8),
Expanded(child: Text(entry.key)),
Text('${entry.value}款'),
],
),
)
),
],
),
),
),
],
),
);
}
4. 性能优化与扩展功能
4.1 性能优化技巧
- 列表优化:
- 使用
ListView.builder和GridView.builder实现懒加载 - 为列表项设置
const构造函数减少重建 - 使用
AutomaticKeepAliveClientMixin保持列表项状态
- 使用
dart复制class StickyNoteCard extends StatelessWidget {
const StickyNoteCard({super.key, required this.note});
final StickyNote note;
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: () {},
child: Column(
children: [
// 卡片内容
],
),
),
);
}
}
- 状态管理优化:
- 使用
Provider实现细粒度状态更新 - 将大对象拆分为多个小状态
- 使用
select方法避免不必要的重建
- 使用
dart复制final notesProvider = Provider<List<StickyNote>>((ref) {
return [];
});
final statsProvider = Provider<CollectionStats>((ref) {
final notes = ref.watch(notesProvider);
return calculateStats(notes);
});
4.2 扩展功能实现
4.2.1 数据持久化
dart复制// 使用shared_preferences保存数据
Future<void> saveStickyNotes() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = jsonEncode(_stickyNotes.map((n) => n.toJson()).toList());
await prefs.setString('sticky_notes', jsonString);
}
Future<void> loadStickyNotes() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString('sticky_notes');
if (jsonString != null) {
final jsonList = jsonDecode(jsonString) as List;
_stickyNotes = jsonList.map((j) => StickyNote.fromJson(j)).toList();
notifyListeners();
}
}
4.2.2 图片管理
dart复制// 使用image_picker选择图片
Future<void> _pickImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_note.photos.add(image.path);
});
}
}
// 显示图片网格
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: _note.photos.length,
itemBuilder: (context, index) => Image.file(
File(_note.photos[index]),
fit: BoxFit.cover,
),
)
5. 鸿蒙系统适配要点
5.1 鸿蒙特性支持
-
分布式能力:
- 使用
ohos_distributed插件实现设备间数据同步 - 支持在手机、平板、智慧屏等多设备间同步便签纸收藏
- 使用
-
原子化服务:
- 将常用功能(如快速添加便签纸)封装为原子化服务
- 支持服务卡片展示收藏统计信息
dart复制// 分布式数据同步示例
void _syncData() async {
final distributed = OhosDistributed();
if (await distributed.checkDistributedPermission()) {
final devices = await distributed.getTrustedDeviceList();
if (devices.isNotEmpty) {
await distributed.sendData(devices.first, jsonEncode(_stickyNotes));
}
}
}
5.2 鸿蒙UI适配
- 主题适配:
- 使用鸿蒙系统的主题色和字体
- 适配鸿蒙的圆角设计和阴影效果
dart复制ThemeData(
platform: TargetPlatform.harmony,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF007DFF), // 鸿蒙主题蓝
),
fontFamily: 'HarmonyOS Sans', // 鸿蒙字体
)
- 交互习惯适配:
- 遵循鸿蒙系统的导航模式
- 适配鸿蒙特色的手势操作
6. 开发经验与避坑指南
6.1 常见问题解决
-
列表卡顿问题:
- 确保列表项高度固定或可预估
- 避免在列表项build方法中进行复杂计算
- 使用
RepaintBoundary隔离复杂子组件
-
状态管理混乱:
- 遵循单一职责原则划分状态
- 使用
Provider或Riverpod等成熟方案 - 避免在Widget树中直接管理业务逻辑
6.2 性能优化心得
-
构建优化:
- 将静态部分提取为常量
- 使用
const构造函数减少重建 - 避免在build方法中创建新对象
-
内存管理:
- 及时释放不再使用的资源
- 对大列表使用
ListView.builder - 对图片使用
cached_network_image等缓存方案
6.3 跨平台适配建议
-
平台差异处理:
- 使用
Platform.isAndroid等判断平台 - 为不同平台提供特定的UI实现
- 测试在不同设备上的表现
- 使用
-
字体和图标适配:
- 准备多套字体文件
- 使用
Icon组件而非图片图标 - 考虑不同平台的图标语义差异
7. 项目总结与展望
通过这个项目,我实现了以下目标:
- 开发了一个功能完整的手账便签纸管理应用
- 掌握了Flutter在鸿蒙系统上的开发技巧
- 实践了状态管理和性能优化的多种方案
未来可以考虑的改进方向:
- 增加云同步功能,支持多设备数据同步
- 实现AR预览功能,可视化展示便签纸效果
- 添加社区分享功能,让用户展示自己的收藏
这个项目的完整代码已开源,欢迎在开源鸿蒙跨平台社区交流讨论。开发过程中最大的收获是深入理解了Flutter的渲染机制和状态管理原理,这对提升应用性能有很大帮助。