Flutter for OpenHarmony 微动漫App实战:收藏功能实现
本文介绍了一个完整的Flutter收藏系统实现,包含UI展示与数据管理。收藏页面使用StatelessWidget结合Provider实现数据监听,处理了空状态提示和列表展示。核心的FavoritesProvider采用双数据结构管理收藏数据,支持快速状态判断和持久化存储。系统实现了收藏状态切换、本地存储同步、异常处理等功能,并通过日志输出便于调试。该方案可直接用于实际项目,提供了良好的用户体验和
通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97
收藏功能是用户留存的关键。用户看到喜欢的番剧,点个收藏,下次打开App就能快速找到。这个功能看起来简单,但要做好需要考虑很多细节:收藏状态的实时同步、数据的持久化存储、列表的滑动删除、空状态的友好提示等等。
这篇文章会完整实现一个收藏系统,从Provider状态管理到页面展示,每一行代码都是项目里实际在用的。

收藏页面的基本结构
收藏页面本身逻辑不复杂,主要是展示收藏列表。因为数据由Provider管理,页面只需要监听变化并渲染即可,所以用StatelessWidget就够了:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/favorites_provider.dart';
import '../widgets/anime_list_tile.dart';
class FavoritesScreen extends StatelessWidget {
const FavoritesScreen({super.key});
引入
provider包来监听收藏数据的变化,AnimeListTile是之前搜索文章里介绍过的列表项组件,这里复用它来展示收藏的动漫。组件复用是Flutter开发的一大优势,写一次到处用。
页面的整体布局
Scaffold搭建基本框架,body部分用Consumer监听Provider:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('我的收藏')),
body: Consumer<FavoritesProvider>(
builder: (context, provider, _) {
Consumer是Provider包提供的Widget,它会自动监听FavoritesProvider的变化。当收藏列表有增删时,builder函数会被重新调用,UI自动更新。这比手动调用setState优雅多了。
空状态的处理
没有收藏时要给用户友好的提示,而不是显示一片空白:
if (provider.favorites.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'还没有收藏任何动漫',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
const SizedBox(height: 8),
Text(
'点击❤️图标来收藏你喜欢的动漫',
style: TextStyle(color: Colors.grey[500], fontSize: 14),
),
],
),
);
}
空状态页面包含三个元素:一个大图标、主提示文字、操作引导文字。图标用空心爱心表示"还没有收藏",颜色用浅灰色不会太抢眼。引导文字告诉用户怎么添加收藏,降低使用门槛。
收藏列表的展示
有收藏数据时用ListView展示:
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: provider.favorites.length,
itemBuilder: (_, i) => AnimeListTile(
anime: provider.favorites[i],
onDelete: () => provider.toggleFavorite(provider.favorites[i]),
),
);
},
),
);
}
}
ListView.builder是懒加载的,只渲染屏幕上可见的项,收藏再多也不会卡。每个列表项传入onDelete回调,这样AnimeListTile的滑动删除功能就能生效了。滑动删除后调用toggleFavorite取消收藏。
FavoritesProvider的设计
收藏功能的核心是Provider,它负责管理收藏数据和持久化存储:
import 'dart:convert';
import 'package:flutter/material.dart';
import '../models/anime.dart';
import '../services/storage_service.dart';
class FavoritesProvider extends ChangeNotifier {
List<Anime> _favorites = [];
final Set<int> _favoriteIds = {};
List<Anime> get favorites => _favorites;
用两个数据结构存储收藏:
_favorites是完整的动漫对象列表,用于页面展示;_favoriteIds是ID集合,用于快速判断某个动漫是否已收藏。Set的查找是O(1)复杂度,比遍历List快得多。
构造函数中加载数据
Provider创建时自动从本地存储加载收藏数据:
FavoritesProvider() {
_loadFavorites();
}
构造函数里调用
_loadFavorites,这样App启动时收藏数据就自动加载好了。用户打开收藏页面能立即看到之前收藏的内容,不需要等待。
从本地存储加载收藏
加载逻辑需要处理各种异常情况:
Future<void> _loadFavorites() async {
try {
print('🔄 FavoritesProvider: 加载收藏...');
await StorageService.instance.init();
final data = StorageService.instance.getStringList('favorites') ?? [];
_favorites = [];
_favoriteIds.clear();
先确保存储服务初始化完成,然后读取
favorites这个key。如果没有数据就返回空列表。在解析之前先清空现有数据,避免重复加载导致数据重复。
解析存储的JSON数据
收藏数据以JSON字符串列表的形式存储:
for (final item in data) {
try {
final anime = Anime.fromJson(json.decode(item));
_favorites.add(anime);
_favoriteIds.add(anime.malId);
} catch (e) {
print('⚠️ 解析收藏项失败: $e');
}
}
print('✅ FavoritesProvider: 已加载 ${_favorites.length} 个收藏');
notifyListeners();
} catch (e) {
print('❌ FavoritesProvider: 加载收藏错误: $e');
}
}
遍历每个JSON字符串,解析成Anime对象。每个解析操作都用try-catch包裹,单条数据损坏不会影响其他数据的加载。解析完成后调用
notifyListeners通知UI更新。日志输出方便调试,上线前可以去掉。
收藏状态的切换
点击收藏按钮时调用这个方法:
void toggleFavorite(Anime anime) {
print('🔄 FavoritesProvider: 切换收藏 ${anime.title}');
if (_favoriteIds.contains(anime.malId)) {
_favorites.removeWhere((e) => e.malId == anime.malId);
_favoriteIds.remove(anime.malId);
print('➖ 已移除收藏');
} else {
_favorites.insert(0, anime);
_favoriteIds.add(anime.malId);
print('➕ 已添加收藏');
}
用
_favoriteIds快速判断当前是收藏还是取消收藏。如果已收藏就从两个数据结构中移除,否则添加到列表开头。新收藏的排在最前面,符合用户的使用习惯。
同步保存到本地存储
状态变化后立即持久化:
// 保存到存储
StorageService.instance.setStringList(
'favorites',
_favorites.map((e) => json.encode(e.toJson())).toList(),
);
print('✅ FavoritesProvider: 当前收藏数: ${_favorites.length}');
notifyListeners();
}
把Anime对象列表转换成JSON字符串列表存储。
map函数遍历每个对象,toJson转成Map,json.encode转成字符串。最后调用notifyListeners让所有监听这个Provider的Widget更新UI。
判断是否已收藏
详情页需要显示当前动漫是否已收藏:
bool isFavorite(int malId) => _favoriteIds.contains(malId);
}
这个方法非常简洁,直接查Set。在详情页的收藏按钮上调用这个方法,根据返回值显示实心或空心爱心图标。因为用的是Set,即使收藏了几百个动漫,查询也是瞬间完成。
在详情页使用收藏功能
收藏按钮通常放在详情页的AppBar里,来看看怎么集成:
AppBar(
actions: [
Consumer<FavoritesProvider>(
builder: (context, provider, _) {
final isFav = provider.isFavorite(anime.malId);
return IconButton(
icon: Icon(
isFav ? Icons.favorite : Icons.favorite_border,
color: isFav ? Colors.red : null,
),
onPressed: () => provider.toggleFavorite(anime),
);
},
),
],
)
用
Consumer包裹收藏按钮,这样收藏状态变化时只有按钮会重建,不会影响整个页面。isFavorite判断当前状态,已收藏显示红色实心爱心,未收藏显示空心爱心。点击时调用toggleFavorite切换状态。
收藏按钮的动画效果
为了让交互更有感觉,可以给收藏按钮加个缩放动画:
class FavoriteButton extends StatefulWidget {
final Anime anime;
const FavoriteButton({super.key, required this.anime});
State<FavoriteButton> createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scale = Tween<double>(begin: 1.0, end: 1.3).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
用
AnimationController控制动画,Tween定义从1.0到1.3的缩放范围。CurvedAnimation让动画有缓入缓出的效果,看起来更自然。SingleTickerProviderStateMixin提供动画需要的vsync。
动画的触发和构建
点击时播放动画,然后反向回到原始大小:
void _onTap() {
_controller.forward().then((_) => _controller.reverse());
context.read<FavoritesProvider>().toggleFavorite(widget.anime);
}
Widget build(BuildContext context) {
return Consumer<FavoritesProvider>(
builder: (context, provider, _) {
final isFav = provider.isFavorite(widget.anime.malId);
return ScaleTransition(
scale: _scale,
child: IconButton(
icon: Icon(
isFav ? Icons.favorite : Icons.favorite_border,
color: isFav ? Colors.red : null,
),
onPressed: _onTap,
),
);
},
);
}
forward播放动画到结束,then里调用reverse反向播放回原始状态。ScaleTransition根据_scale的值缩放子Widget。这样点击收藏按钮时会有个"弹一下"的效果,用户能明确感知到操作成功了。
释放动画资源
别忘了在dispose里释放AnimationController:
void dispose() {
_controller.dispose();
super.dispose();
}
}
AnimationController持有资源,不释放会内存泄漏。这是Flutter动画开发的基本规范,每次用AnimationController都要记得在dispose里清理。
收藏列表的排序
目前新收藏的排在最前面,如果想支持其他排序方式,可以扩展Provider:
enum SortType { newest, oldest, rating, title }
class FavoritesProvider extends ChangeNotifier {
SortType _sortType = SortType.newest;
void setSortType(SortType type) {
_sortType = type;
_sortFavorites();
notifyListeners();
}
void _sortFavorites() {
switch (_sortType) {
case SortType.newest:
// 默认顺序,新收藏在前
break;
case SortType.oldest:
_favorites = _favorites.reversed.toList();
break;
case SortType.rating:
_favorites.sort((a, b) => (b.score ?? 0).compareTo(a.score ?? 0));
break;
case SortType.title:
_favorites.sort((a, b) => a.title.compareTo(b.title));
break;
}
}
}
用枚举定义排序类型,
setSortType方法切换排序方式。按评分排序时处理null值,默认当0分处理。按标题排序用字符串的compareTo方法。排序后调用notifyListeners更新UI。
收藏页面添加排序选项
在AppBar里加个排序按钮:
AppBar(
title: const Text('我的收藏'),
actions: [
PopupMenuButton<SortType>(
icon: const Icon(Icons.sort),
onSelected: (type) {
context.read<FavoritesProvider>().setSortType(type);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: SortType.newest,
child: Text('最近收藏'),
),
const PopupMenuItem(
value: SortType.oldest,
child: Text('最早收藏'),
),
const PopupMenuItem(
value: SortType.rating,
child: Text('评分最高'),
),
const PopupMenuItem(
value: SortType.title,
child: Text('按标题'),
),
],
),
],
)
PopupMenuButton点击后弹出菜单,选择后调用Provider的setSortType方法。菜单项用PopupMenuItem,value是排序类型,child是显示的文字。这样用户就能按自己的喜好排列收藏列表了。
批量管理收藏
如果收藏太多,可以加个批量删除功能:
class FavoritesProvider extends ChangeNotifier {
bool _isSelectionMode = false;
final Set<int> _selectedIds = {};
bool get isSelectionMode => _isSelectionMode;
Set<int> get selectedIds => _selectedIds;
void toggleSelectionMode() {
_isSelectionMode = !_isSelectionMode;
if (!_isSelectionMode) {
_selectedIds.clear();
}
notifyListeners();
}
void toggleSelection(int malId) {
if (_selectedIds.contains(malId)) {
_selectedIds.remove(malId);
} else {
_selectedIds.add(malId);
}
notifyListeners();
}
void deleteSelected() {
_favorites.removeWhere((e) => _selectedIds.contains(e.malId));
for (final id in _selectedIds) {
_favoriteIds.remove(id);
}
_selectedIds.clear();
_isSelectionMode = false;
_saveFavorites();
notifyListeners();
}
}
添加选择模式的状态和选中ID的集合。
toggleSelectionMode切换选择模式,退出时清空选中项。toggleSelection切换单个项的选中状态。deleteSelected批量删除选中的收藏,然后退出选择模式。
小结
收藏功能涉及的知识点不少:Provider状态管理、本地数据持久化、JSON序列化、列表展示、滑动删除、动画效果等。把这些功能组合起来,就是一个完整的收藏系统。
代码结构上,Provider负责数据和逻辑,页面只负责展示,职责分离清晰。这样的架构在OpenHarmony设备上运行稳定,数据同步也很及时。
如果你的App需要收藏功能,这套方案可以直接拿去用,根据实际需求调整细节就行。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)