Flutter 框架跨平台鸿蒙开发 - 表情包本地管理器应用开发教程

Flutter表情包本地管理器应用开发教程

项目简介

表情包本地管理器是一款专为表情包爱好者设计的移动应用,帮助用户高效管理和使用本地存储的表情包。应用采用Flutter框架开发,提供直观的界面和丰富的管理功能,让表情包的收集、分类、使用变得更加便捷。
运行效果图



核心功能

  • 表情包管理:创建、编辑、删除表情包,支持分类和标签
  • 表情浏览:网格和列表两种视图模式,支持搜索和排序
  • 收藏功能:收藏常用表情,快速访问喜爱的内容
  • 使用统计:记录表情使用次数,展示最近使用历史
  • 一键复制:点击表情即可复制到剪贴板,方便分享
  • 多种格式:支持PNG、JPG、GIF、WebP等常见图片格式

技术特点

  • 单文件架构,代码结构清晰简洁
  • Material Design 3设计风格
  • 响应式布局,适配不同屏幕尺寸
  • 丰富的交互动画和用户体验
  • 完整的数据模型和状态管理

项目架构设计

整体架构

EmojiManagerApp
EmojiManagerHomePage
表情包页面
浏览页面
收藏页面
设置页面
数据模型
EmojiPack
EmojiItem
对话框组件
AddPackDialog
PackDetailsDialog
EmojiDetailsDialog

页面结构

应用采用底部导航栏设计,包含四个主要页面:

  1. 表情包页面:管理表情包,支持创建、编辑、删除操作
  2. 浏览页面:浏览所有表情,显示最近使用和全部表情
  3. 收藏页面:管理收藏的表情,快速访问喜爱内容
  4. 设置页面:应用设置和存储管理功能

数据模型设计

EmojiPack 表情包模型

dart 复制代码
class EmojiPack {
  final String id;              // 唯一标识
  final String name;            // 表情包名称
  final String description;     // 描述
  final String coverPath;       // 封面图片路径
  final List<EmojiItem> emojis; // 表情列表
  final DateTime createdAt;     // 创建时间
  final DateTime updatedAt;     // 更新时间
  final EmojiPackType type;     // 表情包类型
  final List<String> tags;      // 标签列表
}

EmojiItem 表情项目模型

dart 复制代码
class EmojiItem {
  final String id;              // 唯一标识
  final String name;            // 表情名称
  final String filePath;        // 文件路径
  final String fileName;        // 文件名
  final EmojiFormat format;     // 文件格式
  final double fileSize;        // 文件大小(KB)
  final int width;              // 图片宽度
  final int height;             // 图片高度
  final DateTime addedAt;       // 添加时间
  final List<String> tags;      // 标签列表
  final int usageCount;         // 使用次数
}

枚举定义

dart 复制代码
// 表情包类型
enum EmojiPackType {
  custom,     // 自定义
  imported,   // 导入的
  favorite,   // 收藏
}

// 表情格式
enum EmojiFormat {
  png,        // PNG格式
  jpg,        // JPG格式
  gif,        // GIF格式
  webp,       // WebP格式
}

// 排序类型
enum SortType {
  name,       // 按名称
  date,       // 按日期
  size,       // 按大小
  usage,      // 按使用次数
}

核心功能实现

1. 主页面结构

主页面使用StatefulWidget实现,包含底部导航栏和对应的页面内容:

dart 复制代码
class _EmojiManagerHomePageState extends State<EmojiManagerHomePage>
    with TickerProviderStateMixin {
  int _selectedIndex = 0;
  late TabController _packsTabController;
  
  // 数据列表
  final List<EmojiPack> _emojiPacks = [];
  final List<EmojiItem> _favoriteEmojis = [];
  final List<EmojiItem> _recentEmojis = [];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: [
        _buildPacksPage(),      // 表情包页面
        _buildBrowsePage(),     // 浏览页面
        _buildFavoritePage(),   // 收藏页面
        _buildSettingsPage(),   // 设置页面
      ][_selectedIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.folder_outlined), label: '表情包'),
          NavigationDestination(icon: Icon(Icons.explore_outlined), label: '浏览'),
          NavigationDestination(icon: Icon(Icons.favorite_outline), label: '收藏'),
          NavigationDestination(icon: Icon(Icons.settings_outlined), label: '设置'),
        ],
      ),
    );
  }
}

2. 表情包管理功能

表情包列表展示

支持网格视图和列表视图两种模式:

dart 复制代码
Widget _buildPacksList(bool onlyFavorites) {
  var filteredPacks = _emojiPacks.where((pack) {
    // 搜索过滤
    if (_searchQuery.isNotEmpty) {
      final query = _searchQuery.toLowerCase();
      if (!pack.name.toLowerCase().contains(query) &&
          !pack.description.toLowerCase().contains(query) &&
          !pack.tags.any((tag) => tag.toLowerCase().contains(query))) {
        return false;
      }
    }
    return true;
  }).toList();

  if (_isGridView) {
    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 0.8,
      ),
      itemCount: filteredPacks.length,
      itemBuilder: (context, index) => _buildPackGridCard(filteredPacks[index]),
    );
  } else {
    return ListView.builder(
      itemCount: filteredPacks.length,
      itemBuilder: (context, index) => _buildPackListCard(filteredPacks[index]),
    );
  }
}
表情包卡片设计
dart 复制代码
Widget _buildPackGridCard(EmojiPack pack) {
  final typeConfig = _typeConfigs[pack.type]!;

  return Card(
    child: InkWell(
      onTap: () => _showPackDetails(pack),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 封面图片
            Expanded(
              child: Container(
                width: double.infinity,
                decoration: BoxDecoration(
                  color: Colors.grey.shade200,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: pack.emojis.isNotEmpty
                    ? ClipRRect(
                        borderRadius: BorderRadius.circular(8),
                        child: _buildEmojiPreview(pack.emojis.first),
                      )
                    : Icon(Icons.emoji_emotions, size: 48, color: Colors.grey.shade400),
              ),
            ),
            // 标题和类型标签
            Row(
              children: [
                Expanded(
                  child: Text(pack.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                ),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                  decoration: BoxDecoration(
                    color: typeConfig.color.withValues(alpha: 0.2),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Icon(typeConfig.icon, size: 14, color: typeConfig.color),
                ),
              ],
            ),
            // 统计信息
            Row(
              children: [
                Icon(Icons.emoji_emotions, size: 14, color: Colors.grey.shade600),
                Text('${pack.emojiCount}个'),
                const SizedBox(width: 12),
                Icon(Icons.storage, size: 14, color: Colors.grey.shade600),
                Text('${(pack.totalSize / 1024).toStringAsFixed(1)}MB'),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

3. 表情使用功能

表情点击复制
dart 复制代码
void _useEmoji(EmojiItem emoji) {
  // 复制到剪贴板
  Clipboard.setData(ClipboardData(text: emoji.name));
  
  // 更新使用次数
  final packIndex = _emojiPacks.indexWhere((pack) => 
      pack.emojis.any((e) => e.id == emoji.id));
  if (packIndex != -1) {
    final pack = _emojiPacks[packIndex];
    final emojiIndex = pack.emojis.indexWhere((e) => e.id == emoji.id);
    if (emojiIndex != -1) {
      final updatedEmoji = pack.emojis[emojiIndex].copyWith(
        usageCount: pack.emojis[emojiIndex].usageCount + 1,
      );
      
      setState(() {
        // 更新表情包中的表情
        final updatedEmojis = List<EmojiItem>.from(pack.emojis);
        updatedEmojis[emojiIndex] = updatedEmoji;
        _emojiPacks[packIndex] = pack.copyWith(emojis: updatedEmojis);
        
        // 更新最近使用列表
        _recentEmojis.removeWhere((e) => e.id == emoji.id);
        _recentEmojis.insert(0, updatedEmoji);
        if (_recentEmojis.length > 20) {
          _recentEmojis.removeLast();
        }
      });
    }
  }

  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('已复制"${emoji.name}"到剪贴板')),
  );
}
表情长按选项
dart 复制代码
void _showEmojiOptions(EmojiItem emoji) {
  final isFavorite = _favoriteEmojis.any((e) => e.id == emoji.id);
  
  showModalBottomSheet(
    context: context,
    builder: (context) => SafeArea(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.copy),
            title: const Text('复制'),
            onTap: () {
              Navigator.pop(context);
              _useEmoji(emoji);
            },
          ),
          ListTile(
            leading: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
            title: Text(isFavorite ? '取消收藏' : '添加收藏'),
            onTap: () {
              Navigator.pop(context);
              _toggleFavorite(emoji);
            },
          ),
          ListTile(
            leading: const Icon(Icons.info),
            title: const Text('详细信息'),
            onTap: () {
              Navigator.pop(context);
              _showEmojiDetails(emoji);
            },
          ),
          ListTile(
            leading: const Icon(Icons.share),
            title: const Text('分享'),
            onTap: () {
              Navigator.pop(context);
              _shareEmoji(emoji);
            },
          ),
        ],
      ),
    ),
  );
}

4. 收藏管理功能

dart 复制代码
void _toggleFavorite(EmojiItem emoji) {
  setState(() {
    final isFavorite = _favoriteEmojis.any((e) => e.id == emoji.id);
    if (isFavorite) {
      _favoriteEmojis.removeWhere((e) => e.id == emoji.id);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('已取消收藏"${emoji.name}"')),
      );
    } else {
      _favoriteEmojis.add(emoji);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('已收藏"${emoji.name}"')),
      );
    }
  });
}

5. 搜索和排序功能

搜索实现
dart 复制代码
// 搜索框UI
TextField(
  controller: _searchController,
  decoration: InputDecoration(
    hintText: '搜索表情包名称或标签...',
    prefixIcon: const Icon(Icons.search),
    suffixIcon: _searchQuery.isNotEmpty
        ? IconButton(
            icon: const Icon(Icons.clear),
            onPressed: () {
              _searchController.clear();
              setState(() {
                _searchQuery = '';
              });
            },
          )
        : null,
    filled: true,
    fillColor: Colors.white,
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
      borderSide: BorderSide.none,
    ),
  ),
  onChanged: (value) {
    setState(() {
      _searchQuery = value;
    });
  },
)
排序功能
dart 复制代码
void _sortPacks(List<EmojiPack> packs) {
  switch (_sortType) {
    case SortType.name:
      packs.sort((a, b) => a.name.compareTo(b.name));
      break;
    case SortType.date:
      packs.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
      break;
    case SortType.size:
      packs.sort((a, b) => b.totalSize.compareTo(a.totalSize));
      break;
    case SortType.usage:
      packs.sort((a, b) {
        final aUsage = a.emojis.fold(0, (sum, emoji) => sum + emoji.usageCount);
        final bUsage = b.emojis.fold(0, (sum, emoji) => sum + emoji.usageCount);
        return bUsage.compareTo(aUsage);
      });
      break;
  }
}

UI组件设计

1. 渐变头部设计

每个页面都使用渐变色头部提升视觉效果:

dart 复制代码
Container(
  padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.amber.shade600, Colors.amber.shade400],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
  ),
  child: Column(
    children: [
      // 标题行
      Row(
        children: [
          const Icon(Icons.emoji_emotions, color: Colors.white, size: 32),
          const SizedBox(width: 12),
          const Text('表情包管理器', style: TextStyle(fontSize: 24, color: Colors.white)),
          const Spacer(),
          IconButton(
            icon: Icon(_isGridView ? Icons.list : Icons.grid_view, color: Colors.white),
            onPressed: () => setState(() => _isGridView = !_isGridView),
          ),
        ],
      ),
      // 搜索框和统计卡片
    ],
  ),
)

2. 统计卡片组件

dart 复制代码
Widget _buildSummaryCard(String title, String value, String unit, IconData icon) {
  return Container(
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.white.withValues(alpha: 0.2),
      borderRadius: BorderRadius.circular(12),
    ),
    child: Column(
      children: [
        Icon(icon, color: Colors.white, size: 20),
        const SizedBox(height: 4),
        Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
        Text(unit, style: const TextStyle(fontSize: 10, color: Colors.white70)),
        Text(title, style: const TextStyle(fontSize: 12, color: Colors.white70)),
      ],
    ),
  );
}

3. 表情卡片组件

dart 复制代码
Widget _buildEmojiCard(EmojiItem emoji, {bool isCompact = false, bool showFavorite = false}) {
  final formatConfig = _formatConfigs[emoji.format]!;
  final isFavorite = _favoriteEmojis.any((e) => e.id == emoji.id);

  return Card(
    child: InkWell(
      onTap: () => _useEmoji(emoji),
      onLongPress: () => _showEmojiOptions(emoji),
      child: Padding(
        padding: EdgeInsets.all(isCompact ? 8 : 12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 表情图片
            Expanded(
              child: Container(
                width: double.infinity,
                decoration: BoxDecoration(
                  color: Colors.grey.shade100,
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Stack(
                  children: [
                    Center(child: _buildEmojiPreview(emoji)),
                    // 格式标签
                    Positioned(
                      top: 4, right: 4,
                      child: Container(
                        padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
                        decoration: BoxDecoration(
                          color: formatConfig.color.withValues(alpha: 0.8),
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(formatConfig.name, style: const TextStyle(fontSize: 8, color: Colors.white)),
                      ),
                    ),
                    // 收藏标记
                    if (isFavorite)
                      const Positioned(
                        top: 4, left: 4,
                        child: Icon(Icons.favorite, size: 16, color: Colors.red),
                      ),
                  ],
                ),
              ),
            ),
            // 表情信息
            Text(emoji.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
            Row(
              children: [
                Icon(Icons.storage, size: 12, color: Colors.grey.shade600),
                Text(emoji.fileSizeText, style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
                if (emoji.usageCount > 0) ...[
                  Icon(Icons.trending_up, size: 12, color: Colors.grey.shade600),
                  Text('${emoji.usageCount}', style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
                ],
              ],
            ),
          ],
        ),
      ),
    ),
  );
}

4. 表情预览组件

dart 复制代码
Widget _buildEmojiPreview(EmojiItem emoji) {
  // 这里应该根据实际文件路径加载图片
  // 由于是示例,使用占位符
  return Container(
    width: 48, height: 48,
    decoration: BoxDecoration(
      color: Colors.amber.shade100,
      borderRadius: BorderRadius.circular(8),
    ),
    child: Center(
      child: Text(
        emoji.name.isNotEmpty ? emoji.name[0] : '😀',
        style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
      ),
    ),
  );
}

对话框组件实现

1. 添加表情包对话框

dart 复制代码
class _AddPackDialog extends StatefulWidget {
  final Function(EmojiPack) onSave;

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('添加表情包'),
      content: SizedBox(
        width: 400, height: 300,
        child: Form(
          key: _formKey,
          child: SingleChildScrollView(
            child: Column(
              children: [
                // 名称输入框
                TextFormField(
                  controller: _nameController,
                  decoration: const InputDecoration(
                    labelText: '表情包名称 *',
                    prefixIcon: Icon(Icons.title),
                  ),
                  validator: (value) => value?.trim().isEmpty == true ? '请输入表情包名称' : null,
                ),
                // 描述输入框
                TextFormField(
                  controller: _descriptionController,
                  decoration: const InputDecoration(
                    labelText: '描述(可选)',
                    prefixIcon: Icon(Icons.description),
                  ),
                  maxLines: 2,
                ),
                // 类型选择
                DropdownButtonFormField<EmojiPackType>(
                  value: _selectedType,
                  decoration: const InputDecoration(
                    labelText: '类型',
                    prefixIcon: Icon(Icons.category),
                  ),
                  items: EmojiPackType.values.map((type) => 
                    DropdownMenuItem(value: type, child: Text(_getTypeText(type)))).toList(),
                  onChanged: (value) => setState(() => _selectedType = value!),
                ),
                // 标签输入框
                TextFormField(
                  controller: _tagsController,
                  decoration: const InputDecoration(
                    labelText: '标签(用逗号分隔)',
                    prefixIcon: Icon(Icons.tag),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
        ElevatedButton(onPressed: _savePack, child: const Text('保存')),
      ],
    );
  }
}

2. 表情包详情对话框

dart 复制代码
class _PackDetailsDialog extends StatelessWidget {
  final EmojiPack pack;
  final Function(EmojiPack) onEdit;
  final VoidCallback onDelete;

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(pack.name),
      content: SizedBox(
        width: 400, height: 500,
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildDetailRow('名称', pack.name),
              _buildDetailRow('描述', pack.description),
              _buildDetailRow('类型', _getTypeText(pack.type)),
              _buildDetailRow('表情数量', '${pack.emojiCount}个'),
              _buildDetailRow('总大小', '${(pack.totalSize / 1024).toStringAsFixed(1)}MB'),
              _buildDetailRow('创建时间', _formatDate(pack.createdAt)),
              _buildDetailRow('更新时间', _formatDate(pack.updatedAt)),
              // 标签展示
              if (pack.tags.isNotEmpty) ...[
                const Text('标签:', style: TextStyle(fontWeight: FontWeight.w500)),
                Wrap(
                  spacing: 8,
                  children: pack.tags.map((tag) => Chip(label: Text(tag))).toList(),
                ),
              ],
              // 表情预览
              if (pack.emojis.isNotEmpty) ...[
                const Text('表情预览:', style: TextStyle(fontWeight: FontWeight.w500)),
                GridView.builder(
                  shrinkWrap: true,
                  physics: const NeverScrollableScrollPhysics(),
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 4,
                    crossAxisSpacing: 8,
                    mainAxisSpacing: 8,
                  ),
                  itemCount: pack.emojis.length > 8 ? 8 : pack.emojis.length,
                  itemBuilder: (context, index) => _buildEmojiPreview(pack.emojis[index]),
                ),
              ],
            ],
          ),
        ),
      ),
      actions: [
        TextButton(onPressed: () { Navigator.pop(context); onDelete(); }, 
                  child: const Text('删除', style: TextStyle(color: Colors.red))),
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
      ],
    );
  }
}

3. 表情详情对话框

dart 复制代码
class _EmojiDetailsDialog extends StatelessWidget {
  final EmojiItem emoji;

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(emoji.name),
      content: SizedBox(
        width: 300, height: 400,
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 表情预览
              Center(
                child: Container(
                  width: 120, height: 120,
                  decoration: BoxDecoration(
                    color: Colors.grey.shade100,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Center(child: _buildEmojiPreview(emoji)),
                ),
              ),
              // 详细信息
              _buildDetailRow('名称', emoji.name),
              _buildDetailRow('文件名', emoji.fileName),
              _buildDetailRow('格式', _getFormatText(emoji.format)),
              _buildDetailRow('大小', emoji.fileSizeText),
              _buildDetailRow('分辨率', emoji.resolutionText),
              _buildDetailRow('使用次数', '${emoji.usageCount}次'),
              _buildDetailRow('添加时间', _formatDate(emoji.addedAt)),
              // 标签展示
              if (emoji.tags.isNotEmpty) ...[
                const Text('标签:', style: TextStyle(fontWeight: FontWeight.w500)),
                Wrap(
                  spacing: 8,
                  children: emoji.tags.map((tag) => Chip(label: Text(tag))).toList(),
                ),
              ],
            ],
          ),
        ),
      ),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
      ],
    );
  }
}

状态管理

1. 本地状态管理

应用使用setState进行简单的本地状态管理:

dart 复制代码
class _EmojiManagerHomePageState extends State<EmojiManagerHomePage> {
  // 数据列表
  final List<EmojiPack> _emojiPacks = [];
  final List<EmojiItem> _favoriteEmojis = [];
  final List<EmojiItem> _recentEmojis = [];
  
  // 搜索和筛选状态
  String _searchQuery = '';
  SortType _sortType = SortType.date;
  bool _isGridView = true;
  
  // 添加表情包
  void _addPack(EmojiPack pack) {
    setState(() {
      _emojiPacks.add(pack);
    });
  }
  
  // 删除表情包
  void _deletePack(String packId) {
    setState(() {
      _emojiPacks.removeWhere((pack) => pack.id == packId);
    });
  }
  
  // 更新表情包
  void _updatePack(EmojiPack updatedPack) {
    setState(() {
      final index = _emojiPacks.indexWhere((p) => p.id == updatedPack.id);
      if (index != -1) {
        _emojiPacks[index] = updatedPack;
      }
    });
  }
}

2. 数据持久化扩展

虽然当前版本使用内存存储,但可以轻松扩展为持久化存储:

dart 复制代码
// 保存数据到本地存储
Future<void> _saveData() async {
  final prefs = await SharedPreferences.getInstance();
  
  // 保存表情包数据
  final packsJson = _emojiPacks.map((pack) => pack.toJson()).toList();
  await prefs.setString('emoji_packs', jsonEncode(packsJson));
  
  // 保存收藏数据
  final favoritesJson = _favoriteEmojis.map((emoji) => emoji.toJson()).toList();
  await prefs.setString('favorite_emojis', jsonEncode(favoritesJson));
  
  // 保存最近使用数据
  final recentJson = _recentEmojis.map((emoji) => emoji.toJson()).toList();
  await prefs.setString('recent_emojis', jsonEncode(recentJson));
}

// 从本地存储加载数据
Future<void> _loadData() async {
  final prefs = await SharedPreferences.getInstance();
  
  // 加载表情包数据
  final packsString = prefs.getString('emoji_packs');
  if (packsString != null) {
    final packsList = jsonDecode(packsString) as List;
    _emojiPacks.addAll(packsList.map((json) => EmojiPack.fromJson(json)));
  }
  
  // 加载收藏数据
  final favoritesString = prefs.getString('favorite_emojis');
  if (favoritesString != null) {
    final favoritesList = jsonDecode(favoritesString) as List;
    _favoriteEmojis.addAll(favoritesList.map((json) => EmojiItem.fromJson(json)));
  }
  
  // 加载最近使用数据
  final recentString = prefs.getString('recent_emojis');
  if (recentString != null) {
    final recentList = jsonDecode(recentString) as List;
    _recentEmojis.addAll(recentList.map((json) => EmojiItem.fromJson(json)));
  }
}

工具方法实现

1. 日期格式化

dart 复制代码
String _formatDate(DateTime date) {
  return '${date.month}/${date.day}';  // 简短格式:月/日
}

String _formatFullDate(DateTime date) {
  return '${date.year}年${date.month}月${date.day}日';  // 完整格式
}

2. 文件大小格式化

dart 复制代码
String get fileSizeText {
  if (fileSize < 1024) {
    return '${fileSize.toStringAsFixed(1)} KB';
  } else {
    return '${(fileSize / 1024).toStringAsFixed(1)} MB';
  }
}

3. 类型和格式转换

dart 复制代码
String _getTypeText(EmojiPackType type) {
  switch (type) {
    case EmojiPackType.custom:
      return '自定义';
    case EmojiPackType.imported:
      return '导入';
    case EmojiPackType.favorite:
      return '收藏';
  }
}

String _getFormatText(EmojiFormat format) {
  switch (format) {
    case EmojiFormat.png:
      return 'PNG';
    case EmojiFormat.jpg:
      return 'JPG';
    case EmojiFormat.gif:
      return 'GIF';
    case EmojiFormat.webp:
      return 'WebP';
  }
}

String _getSortTypeName(SortType type) {
  switch (type) {
    case SortType.name:
      return '按名称';
    case SortType.date:
      return '按日期';
    case SortType.size:
      return '按大小';
    case SortType.usage:
      return '按使用次数';
  }
}

4. 搜索和筛选方法

dart 复制代码
// 搜索表情包
List<EmojiPack> _searchPacks(String query) {
  if (query.isEmpty) return _emojiPacks;
  
  final lowerQuery = query.toLowerCase();
  return _emojiPacks.where((pack) {
    return pack.name.toLowerCase().contains(lowerQuery) ||
           pack.description.toLowerCase().contains(lowerQuery) ||
           pack.tags.any((tag) => tag.toLowerCase().contains(lowerQuery));
  }).toList();
}

// 搜索表情
List<EmojiItem> _searchEmojis(String query) {
  final allEmojis = _emojiPacks.expand((pack) => pack.emojis).toList();
  
  if (query.isEmpty) return allEmojis;
  
  final lowerQuery = query.toLowerCase();
  return allEmojis.where((emoji) {
    return emoji.name.toLowerCase().contains(lowerQuery) ||
           emoji.tags.any((tag) => tag.toLowerCase().contains(lowerQuery));
  }).toList();
}

// 按类型筛选表情包
List<EmojiPack> _filterPacksByType(EmojiPackType type) {
  return _emojiPacks.where((pack) => pack.type == type).toList();
}

// 按格式筛选表情
List<EmojiItem> _filterEmojisByFormat(EmojiFormat format) {
  final allEmojis = _emojiPacks.expand((pack) => pack.emojis).toList();
  return allEmojis.where((emoji) => emoji.format == format).toList();
}

功能扩展建议

1. 文件导入功能

dart 复制代码
// 添加文件选择依赖
dependencies:
  file_picker: ^8.1.2
  path_provider: ^2.1.4

// 实现文件导入
Future<void> _importEmojis() async {
  try {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.image,
      allowMultiple: true,
    );
    
    if (result != null) {
      final directory = await getApplicationDocumentsDirectory();
      final emojiDir = Directory('${directory.path}/emojis');
      if (!await emojiDir.exists()) {
        await emojiDir.create(recursive: true);
      }
      
      for (final file in result.files) {
        if (file.path != null) {
          final originalFile = File(file.path!);
          final fileName = file.name;
          final newPath = '${emojiDir.path}/$fileName';
          
          // 复制文件到应用目录
          await originalFile.copy(newPath);
          
          // 创建表情项目
          final emoji = EmojiItem(
            id: DateTime.now().millisecondsSinceEpoch.toString(),
            name: fileName.split('.').first,
            filePath: newPath,
            fileName: fileName,
            format: _getFormatFromExtension(fileName.split('.').last),
            fileSize: await originalFile.length() / 1024,
            addedAt: DateTime.now(),
          );
          
          // 添加到默认表情包
          _addEmojiToDefaultPack(emoji);
        }
      }
      
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('成功导入${result.files.length}个表情')),
      );
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('导入失败:$e')),
    );
  }
}

2. 表情包导出功能

dart 复制代码
// 添加压缩依赖
dependencies:
  archive: ^3.6.1

// 实现表情包导出
Future<void> _exportEmojiPack(EmojiPack pack) async {
  try {
    final directory = await getApplicationDocumentsDirectory();
    final exportDir = Directory('${directory.path}/exports');
    if (!await exportDir.exists()) {
      await exportDir.create(recursive: true);
    }
    
    // 创建ZIP压缩包
    final archive = Archive();
    
    // 添加表情包信息文件
    final packInfo = {
      'name': pack.name,
      'description': pack.description,
      'type': pack.type.toString(),
      'tags': pack.tags,
      'createdAt': pack.createdAt.toIso8601String(),
    };
    final infoJson = jsonEncode(packInfo);
    archive.addFile(ArchiveFile('pack_info.json', infoJson.length, infoJson.codeUnits));
    
    // 添加表情文件
    for (final emoji in pack.emojis) {
      final file = File(emoji.filePath);
      if (await file.exists()) {
        final bytes = await file.readAsBytes();
        archive.addFile(ArchiveFile(emoji.fileName, bytes.length, bytes));
      }
    }
    
    // 保存ZIP文件
    final zipEncoder = ZipEncoder();
    final zipBytes = zipEncoder.encode(archive);
    final zipFile = File('${exportDir.path}/${pack.name}.zip');
    await zipFile.writeAsBytes(zipBytes!);
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('表情包已导出到:${zipFile.path}')),
    );
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('导出失败:$e')),
    );
  }
}

3. 表情分享功能

dart 复制代码
// 添加分享依赖
dependencies:
  share_plus: ^10.1.2

// 实现表情分享
Future<void> _shareEmoji(EmojiItem emoji) async {
  try {
    final file = File(emoji.filePath);
    if (await file.exists()) {
      await Share.shareXFiles(
        [XFile(emoji.filePath)],
        text: emoji.name,
        subject: '分享表情:${emoji.name}',
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('表情文件不存在')),
      );
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('分享失败:$e')),
    );
  }
}

// 分享表情包
Future<void> _shareEmojiPack(EmojiPack pack) async {
  final packInfo = '''
表情包:${pack.name}
描述:${pack.description}
表情数量:${pack.emojiCount}个
总大小:${(pack.totalSize / 1024).toStringAsFixed(1)}MB
标签:${pack.tags.join(', ')}
  ''';
  
  await Share.share(
    packInfo,
    subject: '分享表情包:${pack.name}',
  );
}

4. 表情搜索优化

dart 复制代码
// 实现模糊搜索
class FuzzySearch {
  static double similarity(String a, String b) {
    if (a == b) return 1.0;
    if (a.isEmpty || b.isEmpty) return 0.0;
    
    final longer = a.length > b.length ? a : b;
    final shorter = a.length > b.length ? b : a;
    
    if (longer.length == 0) return 1.0;
    
    return (longer.length - editDistance(longer, shorter)) / longer.length;
  }
  
  static int editDistance(String a, String b) {
    final matrix = List.generate(
      a.length + 1,
      (i) => List.generate(b.length + 1, (j) => 0),
    );
    
    for (int i = 0; i <= a.length; i++) {
      matrix[i][0] = i;
    }
    for (int j = 0; j <= b.length; j++) {
      matrix[0][j] = j;
    }
    
    for (int i = 1; i <= a.length; i++) {
      for (int j = 1; j <= b.length; j++) {
        final cost = a[i - 1] == b[j - 1] ? 0 : 1;
        matrix[i][j] = [
          matrix[i - 1][j] + 1,
          matrix[i][j - 1] + 1,
          matrix[i - 1][j - 1] + cost,
        ].reduce((a, b) => a < b ? a : b);
      }
    }
    
    return matrix[a.length][b.length];
  }
}

// 使用模糊搜索
List<EmojiItem> _fuzzySearchEmojis(String query) {
  final allEmojis = _emojiPacks.expand((pack) => pack.emojis).toList();
  
  if (query.isEmpty) return allEmojis;
  
  final results = allEmojis.map((emoji) {
    final similarity = FuzzySearch.similarity(
      emoji.name.toLowerCase(),
      query.toLowerCase(),
    );
    return MapEntry(emoji, similarity);
  }).where((entry) => entry.value > 0.3).toList();
  
  results.sort((a, b) => b.value.compareTo(a.value));
  
  return results.map((entry) => entry.key).toList();
}

5. 表情标签管理

dart 复制代码
// 标签管理器
class TagManager {
  static final Set<String> _allTags = {};
  
  static void addTag(String tag) {
    _allTags.add(tag.trim().toLowerCase());
  }
  
  static void addTags(List<String> tags) {
    _allTags.addAll(tags.map((tag) => tag.trim().toLowerCase()));
  }
  
  static List<String> getAllTags() {
    return _allTags.toList()..sort();
  }
  
  static List<String> searchTags(String query) {
    if (query.isEmpty) return getAllTags();
    
    final lowerQuery = query.toLowerCase();
    return _allTags
        .where((tag) => tag.contains(lowerQuery))
        .toList()
      ..sort();
  }
  
  static List<String> getPopularTags(List<EmojiPack> packs) {
    final tagCounts = <String, int>{};
    
    for (final pack in packs) {
      for (final tag in pack.tags) {
        tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
      }
      
      for (final emoji in pack.emojis) {
        for (final tag in emoji.tags) {
          tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
        }
      }
    }
    
    final sortedTags = tagCounts.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value));
    
    return sortedTags.take(10).map((entry) => entry.key).toList();
  }
}

// 标签输入组件
class TagInputWidget extends StatefulWidget {
  final List<String> initialTags;
  final Function(List<String>) onTagsChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 标签输入框
        TextField(
          controller: _tagController,
          decoration: InputDecoration(
            labelText: '添加标签',
            suffixIcon: IconButton(
              icon: const Icon(Icons.add),
              onPressed: _addTag,
            ),
          ),
          onSubmitted: (_) => _addTag(),
        ),
        // 已添加的标签
        if (_tags.isNotEmpty) ...[
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            children: _tags.map((tag) {
              return Chip(
                label: Text(tag),
                onDeleted: () => _removeTag(tag),
              );
            }).toList(),
          ),
        ],
        // 推荐标签
        const SizedBox(height: 8),
        const Text('推荐标签:', style: TextStyle(fontWeight: FontWeight.w500)),
        Wrap(
          spacing: 8,
          children: TagManager.getPopularTags(_emojiPacks).map((tag) {
            return ActionChip(
              label: Text(tag),
              onPressed: () => _addRecommendedTag(tag),
            );
          }).toList(),
        ),
      ],
    );
  }
}

性能优化策略

1. 图片加载优化

dart 复制代码
// 使用缓存图片加载
class CachedImageWidget extends StatelessWidget {
  final String imagePath;
  final double? width;
  final double? height;

  const CachedImageWidget({
    super.key,
    required this.imagePath,
    this.width,
    this.height,
  });

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List?>(
      future: _loadImageBytes(),
      builder: (context, snapshot) {
        if (snapshot.hasData && snapshot.data != null) {
          return Image.memory(
            snapshot.data!,
            width: width,
            height: height,
            fit: BoxFit.cover,
            errorBuilder: (context, error, stackTrace) {
              return _buildPlaceholder();
            },
          );
        } else if (snapshot.hasError) {
          return _buildPlaceholder();
        } else {
          return _buildLoadingPlaceholder();
        }
      },
    );
  }

  Future<Uint8List?> _loadImageBytes() async {
    try {
      final file = File(imagePath);
      if (await file.exists()) {
        return await file.readAsBytes();
      }
    } catch (e) {
      print('Error loading image: $e');
    }
    return null;
  }

  Widget _buildPlaceholder() {
    return Container(
      width: width ?? 48,
      height: height ?? 48,
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: BorderRadius.circular(8),
      ),
      child: const Icon(Icons.image, color: Colors.grey),
    );
  }

  Widget _buildLoadingPlaceholder() {
    return Container(
      width: width ?? 48,
      height: height ?? 48,
      decoration: BoxDecoration(
        color: Colors.grey.shade100,
        borderRadius: BorderRadius.circular(8),
      ),
      child: const Center(
        child: SizedBox(
          width: 16,
          height: 16,
          child: CircularProgressIndicator(strokeWidth: 2),
        ),
      ),
    );
  }
}

2. 列表性能优化

dart 复制代码
// 使用ListView.builder进行懒加载
ListView.builder(
  itemCount: filteredEmojis.length,
  itemBuilder: (context, index) {
    final emoji = filteredEmojis[index];
    return _buildEmojiCard(emoji);
  },
  // 添加缓存范围
  cacheExtent: 1000,
)

// 使用AutomaticKeepAliveClientMixin保持页面状态
class _BrowsePageState extends State<BrowsePage> 
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return /* 页面内容 */;
  }
}

3. 搜索性能优化

dart 复制代码
// 使用防抖动搜索
Timer? _searchTimer;

void _onSearchChanged(String query) {
  _searchTimer?.cancel();
  _searchTimer = Timer(const Duration(milliseconds: 300), () {
    setState(() {
      _searchQuery = query;
    });
  });
}

// 建立搜索索引
class SearchIndex {
  final Map<String, List<EmojiItem>> _nameIndex = {};
  final Map<String, List<EmojiItem>> _tagIndex = {};
  
  void buildIndex(List<EmojiItem> emojis) {
    _nameIndex.clear();
    _tagIndex.clear();
    
    for (final emoji in emojis) {
      // 按名称建立索引
      final nameWords = emoji.name.toLowerCase().split(' ');
      for (final word in nameWords) {
        _nameIndex.putIfAbsent(word, () => []).add(emoji);
      }
      
      // 按标签建立索引
      for (final tag in emoji.tags) {
        _tagIndex.putIfAbsent(tag.toLowerCase(), () => []).add(emoji);
      }
    }
  }
  
  List<EmojiItem> search(String query) {
    final queryWords = query.toLowerCase().split(' ');
    final results = <EmojiItem>{};
    
    for (final word in queryWords) {
      results.addAll(_nameIndex[word] ?? []);
      results.addAll(_tagIndex[word] ?? []);
    }
    
    return results.toList();
  }
}

4. 内存管理

dart 复制代码
// 及时释放资源
@override
void dispose() {
  _searchController.dispose();
  _packsTabController.dispose();
  _searchTimer?.cancel();
  super.dispose();
}

// 使用弱引用管理大量对象
class EmojiCache {
  final Map<String, WeakReference<Uint8List>> _cache = {};
  
  Future<Uint8List?> getImageBytes(String path) async {
    final weakRef = _cache[path];
    final cached = weakRef?.target;
    
    if (cached != null) {
      return cached;
    }
    
    try {
      final file = File(path);
      if (await file.exists()) {
        final bytes = await file.readAsBytes();
        _cache[path] = WeakReference(bytes);
        return bytes;
      }
    } catch (e) {
      print('Error loading image: $e');
    }
    
    return null;
  }
  
  void clearCache() {
    _cache.clear();
  }
}

测试指南

1. 单元测试

dart 复制代码
// test/models/emoji_pack_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:emoji_manager/models/emoji_pack.dart';

void main() {
  group('EmojiPack Tests', () {
    test('should calculate total size correctly', () {
      final emojis = [
        EmojiItem(id: '1', name: 'emoji1', filePath: '', fileName: '', 
                  addedAt: DateTime.now(), fileSize: 100),
        EmojiItem(id: '2', name: 'emoji2', filePath: '', fileName: '', 
                  addedAt: DateTime.now(), fileSize: 200),
      ];
      
      final pack = EmojiPack(
        id: '1',
        name: 'Test Pack',
        createdAt: DateTime.now(),
        updatedAt: DateTime.now(),
        emojis: emojis,
      );
      
      expect(pack.totalSize, equals(300.0));
      expect(pack.emojiCount, equals(2));
    });
    
    test('should create copy with updated values', () {
      final original = EmojiPack(
        id: '1',
        name: 'Original',
        createdAt: DateTime.now(),
        updatedAt: DateTime.now(),
      );
      
      final updated = original.copyWith(name: 'Updated');
      
      expect(updated.name, equals('Updated'));
      expect(updated.id, equals('1'));
    });
  });
}

2. Widget测试

dart 复制代码
// test/widgets/emoji_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:emoji_manager/main.dart';

void main() {
  testWidgets('EmojiCard displays correct information', (tester) async {
    final emoji = EmojiItem(
      id: '1',
      name: 'Test Emoji',
      filePath: 'test.png',
      fileName: 'test.png',
      addedAt: DateTime.now(),
      fileSize: 50.0,
      usageCount: 5,
    );
    
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: EmojiCard(emoji: emoji),
        ),
      ),
    );
    
    expect(find.text('Test Emoji'), findsOneWidget);
    expect(find.text('50.0 KB'), findsOneWidget);
    expect(find.text('5'), findsOneWidget);
  });
}

3. 集成测试

dart 复制代码
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:emoji_manager/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('Emoji Manager App Tests', () {
    testWidgets('should add new emoji pack', (tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // 点击添加按钮
      await tester.tap(find.byType(FloatingActionButton));
      await tester.pumpAndSettle();
      
      // 填写表单
      await tester.enterText(find.byKey(const Key('name_field')), 'Test Pack');
      await tester.enterText(find.byKey(const Key('description_field')), 'Test Description');
      
      // 保存表情包
      await tester.tap(find.text('保存'));
      await tester.pumpAndSettle();
      
      // 验证表情包已添加
      expect(find.text('Test Pack'), findsOneWidget);
    });
    
    testWidgets('should navigate between tabs', (tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // 点击浏览标签
      await tester.tap(find.text('浏览'));
      await tester.pumpAndSettle();
      
      expect(find.text('表情浏览'), findsOneWidget);
      
      // 点击收藏标签
      await tester.tap(find.text('收藏'));
      await tester.pumpAndSettle();
      
      expect(find.text('我的收藏'), findsOneWidget);
    });
  });
}

部署指南

1. Android部署

bash 复制代码
# 构建APK
flutter build apk --release

# 构建App Bundle(推荐用于Google Play)
flutter build appbundle --release

# 安装到设备
flutter install

2. iOS部署

bash 复制代码
# 构建iOS应用
flutter build ios --release

# 使用Xcode打开项目进行签名和发布
open ios/Runner.xcworkspace

3. Web部署

bash 复制代码
# 构建Web版本
flutter build web --release

# 部署到服务器
# 将build/web目录下的文件上传到Web服务器

4. 桌面应用部署

bash 复制代码
# Windows
flutter build windows --release

# macOS
flutter build macos --release

# Linux
flutter build linux --release

项目总结

技术亮点

  1. 简洁实用的设计:专注核心功能,界面简洁直观
  2. 完整的管理功能:表情包创建、分类、搜索、收藏一应俱全
  3. 优秀的用户体验:一键复制、长按选项、使用统计等贴心功能
  4. 灵活的视图模式:支持网格和列表两种浏览方式
  5. 强大的搜索功能:支持名称、标签多维度搜索
  6. 扩展性强:提供了丰富的功能扩展建议

学习价值

  • Flutter基础:Widget组合、状态管理、导航等核心概念
  • UI设计:Material Design 3组件使用和自定义样式
  • 数据管理:模型设计、列表操作、搜索筛选
  • 文件操作:图片加载、文件管理、数据持久化
  • 用户体验:交互设计、动画效果、性能优化

这个表情包本地管理器应用展示了Flutter在工具类应用开发中的实用性,从简单的需求出发,构建了一个功能完整、体验良好的应用。通过这个项目,开发者可以学习到如何用Flutter构建实用的工具应用,掌握文件管理、数据处理、用户交互等关键技能。

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

相关推荐
不会写代码0002 小时前
Flutter 框架跨平台鸿蒙开发 - 节日礼物清单应用开发教程
flutter·华为·harmonyos·节日
摘星编程2 小时前
React Native鸿蒙:LayoutAnimation配置弹簧动画
react native·react.js·harmonyos
Mr. Sun_2 小时前
华为云山系统交换机堆叠
华为·云山·专用线缆堆叠
深海的鲸同学 luvi2 小时前
在鸿蒙设备上使用NexServer快速部署网站
harmonyos·网站部署·nexserver
彭不懂赶紧问2 小时前
鸿蒙NEXT开发浅进阶到精通16:从零调试鸿蒙内置AI类API文字转语音场景
华为·harmonyos·鸿蒙·文字转语音
南村群童欺我老无力.2 小时前
Flutter 框架跨平台鸿蒙开发 - 屏幕尺子工具应用开发教程
flutter·华为·harmonyos
一只大侠的侠2 小时前
从环境搭建到工程运行:OpenHarmony版Flutter全流程实战
flutter
猛扇赵四那边好嘴.2 小时前
Flutter 框架跨平台鸿蒙开发 - 每日心情日记应用开发教程
flutter·华为·harmonyos
不会写代码0002 小时前
Flutter 框架跨平台鸿蒙开发 - 学习计划制定器开发教程
学习·flutter·华为·harmonyos