通过网盘分享的文件: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方法。菜单项用PopupMenuItemvalue是排序类型,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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐