在这里插入图片描述

家庭照片那么多,总有几张特别喜欢的。

收藏功能让用户能快速找到这些照片,今天来实现这个页面。

页面定位

收藏页面是一个纯展示页面,显示所有被标记为收藏的照片。

逻辑简单,但细节处理好了用户体验会很不错。

收藏功能是相册App的标配,用户在浏览照片时看到喜欢的,点一下爱心就能收藏。收藏页面把这些照片集中展示,方便用户随时查看。页面设计上要简洁明了,不需要复杂的交互,重点是照片的展示效果。空状态的处理也很重要,要引导新用户知道怎么收藏照片。整个页面的实现虽然简单,但每个细节都要考虑到用户体验。

基础结构

收藏页面没有复杂的状态管理,用StatelessWidget就够了:

class FavoritesScreen extends StatelessWidget {
  const FavoritesScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的收藏'),
      ),

页面标题"我的收藏",简单直接。

没有额外的操作按钮,保持界面清爽。

StatelessWidget配合Consumer就能实现数据响应。

StatelessWidget是无状态组件,适合这种纯展示的页面。虽然页面本身没有状态,但通过Consumer监听Provider的数据变化,当用户在其他页面收藏或取消收藏照片时,这个页面会自动刷新。AppBar只有标题,没有其他按钮,保持界面简洁。这种设计让用户的注意力集中在照片上,不会被其他元素干扰。

数据监听

Consumer监听收藏数据的变化:

      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final favorites = provider.favoritePhotos;

favoritePhotosAlbumProvider里的一个getter,返回所有收藏的照片。

当用户在其他页面取消收藏时,这个列表会自动更新。

Consumer会在数据变化时重新构建UI。

Consumer是Provider的核心组件,它监听AlbumProvider的变化。favoritePhotos是一个计算属性,每次访问都会过滤出isFavorite为true的照片。这种设计的好处是数据总是最新的,不需要手动刷新。当用户在照片详情页点击收藏按钮时,Provider会调用notifyListeners通知所有监听者,Consumer收到通知后会重新执行builder函数,UI就更新了。这个过程是自动的,开发者不需要写任何刷新逻辑。

空状态处理

没有收藏时要给用户友好的提示:

          if (favorites.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey[300]),
                  SizedBox(height: 16.h),
                  Text(
                    '暂无收藏的照片',
                    style: TextStyle(color: Colors.grey, fontSize: 16.sp),
                  ),

空心爱心图标暗示"还没有收藏"。

主提示文字告诉用户当前状态。

图标用浅灰色,不会太突兀。

空状态设计是提升用户体验的关键。favorite_border是空心爱心图标,和收藏的实心爱心形成对比,视觉上很直观。图标尺寸64像素,足够大,用户一眼就能看到。颜色用grey[300],是个很浅的灰色,既能引起注意又不会太抢眼。主提示文字"暂无收藏的照片"说明当前状态,让用户知道这不是出错了,只是还没有收藏内容。这种友好的提示比空白页面好得多。

                  SizedBox(height: 8.h),
                  Text(
                    '点击照片上的❤️可以收藏',
                    style: TextStyle(color: Colors.grey[400], fontSize: 14.sp),
                  ),
                ],
              ),
            );
          }

副提示告诉用户怎么收藏照片,降低学习成本。

用emoji爱心让文字更生动。

字体颜色比主提示更浅,形成层次感。

副提示是引导性文字,告诉用户下一步该做什么。"点击照片上的❤️可以收藏"这句话很直白,新用户看了就知道怎么操作。用emoji爱心代替文字描述,更直观也更生动。字体颜色用grey[400],比主提示的grey更浅,形成视觉层次。这种分层的提示设计,让信息的重要程度一目了然,用户能快速抓住重点。

网格布局

有收藏时用网格展示照片:

          return GridView.builder(
            padding: EdgeInsets.all(16.w),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              mainAxisSpacing: 4.w,
              crossAxisSpacing: 4.w,
            ),
            itemCount: favorites.length,

三列网格布局,和搜索结果页保持一致。

间距4,照片之间有细小的缝隙。

GridView.builder按需构建,性能更好。

GridView.builder是懒加载的网格组件,只构建可见区域的item,滚动时动态创建和销毁,内存占用可控。crossAxisCount设为3,在手机屏幕上显示3列照片,既不会太小也不会太大。mainAxisSpacing和crossAxisSpacing都设为4,照片之间有4像素的间距,既能区分不同照片,又不会浪费太多空间。padding设为16.w给网格四周留出呼吸空间,避免照片紧贴屏幕边缘。这种布局和搜索结果页保持一致,用户在不同页面看到相同的布局,学习成本更低。

照片卡片

每张照片用Stack叠加收藏标记:

            itemBuilder: (context, index) {
              final photo = favorites[index];
              return InkWell(
                onTap: () => Navigator.push(
                  context,
                  MaterialPageRoute(builder: (_) => PhotoDetailScreen(photo: photo)),
                ),
                child: Stack(
                  fit: StackFit.expand,
                  children: [

InkWell包裹让整个卡片可点击。

点击跳转到照片详情页。

Stack用来叠加照片和收藏图标。

InkWell提供Material Design的水波纹点击效果,让交互更有质感。onTap回调处理点击事件,跳转到PhotoDetailScreen查看照片详情。Stack是层叠布局组件,fit设为expand让子组件填满整个空间。底层是Container显示照片(这里用颜色块模拟),上层用Positioned定位收藏图标。这种层叠设计让收藏标记浮在照片上方,视觉效果更好。实际项目中,Container里应该用Image.network或Image.file加载真实图片,配合缓存机制提升加载速度。

                    Container(
                      color: _getColorForPhoto(photo.url),
                      child: Center(
                        child: Icon(Icons.photo, color: Colors.white.withOpacity(0.5)),
                      ),
                    ),

底层是照片占位,用颜色块模拟。

实际项目中换成Image组件加载真实图片。

半透明图标作为占位符。

Container的color属性设置背景色,_getColorForPhoto方法根据照片URL生成颜色。Center让图标居中显示,Icons.photo是照片图标,颜色用白色50%透明度,作为占位符。实际项目中这里应该这样写:Image.network(photo.url, fit: BoxFit.cover),fit设为cover让图片填满容器,超出部分裁剪。还可以加loading和error的处理,比如loadingBuilder显示加载动画,errorBuilder显示错误图标。配合cached_network_image这个库,能实现图片缓存,避免重复下载,提升用户体验。

                    Positioned(
                      top: 4.w,
                      right: 4.w,
                      child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
                    ),
                  ],
                ),
              );
            },
          );
        },
      ),
    );
  }

右上角叠加红色实心爱心,表示已收藏。

Positioned精确控制图标位置。

图标大小20,不会太大遮挡照片。

Positioned是Stack的定位组件,top和right都设为4.w,让图标距离右上角4像素。Icons.favorite是实心爱心图标,颜色用红色,这是收藏的标准颜色,用户一看就懂。size设为20.sp,不会太大遮挡照片内容,也不会太小看不清。这个收藏标记是只读的,不能点击取消收藏,因为收藏页面的定位是展示,取消收藏的操作应该在照片详情页进行。这种设计让功能职责更清晰,避免用户误操作。

颜色生成方法

照片占位颜色的生成:

  Color _getColorForPhoto(String url) {
    final colors = [
      Colors.pink[200]!,
      Colors.purple[200]!,
      Colors.blue[200]!,
      Colors.teal[200]!,
    ];
    return colors[url.hashCode % colors.length];
  }
}

四种柔和的颜色轮换使用。

用URL的哈希值取模,同一张照片颜色固定。

200色阶比较淡,适合做背景。

_getColorForPhoto方法生成照片的占位颜色。colors数组包含四种颜色,都是200色阶,比较淡,作为背景不会太抢眼。url.hashCode获取字符串的哈希值,是个整数。对colors.length取模,结果在0到3之间,用作数组索引。这样同一个URL总是得到相同的颜色,不同URL的颜色分布比较均匀。哈希算法保证了颜色的随机性和一致性,用户每次打开页面看到的颜色都一样,不会闪烁变化。实际项目中用真实图片后,这个方法就不需要了,但在开发阶段用颜色块能快速验证布局效果。

Provider中的实现

AlbumProvider里的favoritePhotosgetter:

List<PhotoModel> get favoritePhotos {
  return _photos.where((p) => p.isFavorite).toList();
}

过滤出所有isFavorite为true的照片。

每次访问都会重新计算,保证数据最新。

返回新列表,不会影响原数据。

favoritePhotos是一个getter,不是普通的属性。每次访问都会执行where方法,过滤出isFavorite为true的照片。where返回的是Iterable,调用toList转成List。这种实时计算的方式保证数据总是最新的,不需要维护一个单独的收藏列表。缺点是每次访问都要遍历整个_photos列表,数据量大时可能有性能问题。优化方案是在Provider里维护一个_favoritePhotos列表,当照片的收藏状态变化时同步更新这个列表,这样favoritePhotos直接返回_favoritePhotos,不需要每次都过滤。

收藏状态切换

在照片详情页切换收藏状态:

void toggleFavorite(String photoId) {
  final index = _photos.indexWhere((p) => p.id == photoId);
  if (index != -1) {
    _photos[index] = _photos[index].copyWith(
      isFavorite: !_photos[index].isFavorite,
    );
    notifyListeners();
  }
}

找到对应照片,切换isFavorite状态。

notifyListeners通知所有监听者,收藏页面会自动刷新。

copyWith创建新对象,符合不可变数据原则。

toggleFavorite方法实现收藏状态的切换。indexWhere查找指定ID的照片,返回索引。如果找到了(index不等于-1),用copyWith创建新对象,isFavorite取反。copyWith是一个常见的模式,创建一个新对象,大部分属性复制原对象,只修改指定的属性。这符合不可变数据的原则,不直接修改原对象,而是创建新对象替换。最后调用notifyListeners通知所有监听者,Consumer收到通知后会重新构建UI。这个方法通常在照片详情页调用,用户点击收藏按钮时触发。

交互流程

用户的典型操作流程:

// 在照片详情页点击收藏按钮
IconButton(
  icon: Icon(
    currentPhoto.isFavorite ? Icons.favorite : Icons.favorite_border,
    color: currentPhoto.isFavorite ? Colors.red : Colors.white,
  ),
  onPressed: () => provider.toggleFavorite(photo.id),
);

收藏按钮根据状态显示不同图标和颜色。

点击后调用toggleFavorite切换状态。

收藏页面会实时更新,不需要手动刷新。

照片详情页的收藏按钮是个IconButton,图标根据currentPhoto.isFavorite动态选择。已收藏显示实心爱心Icons.favorite,颜色用红色;未收藏显示空心爱心Icons.favorite_border,颜色用白色(假设详情页背景是深色)。onPressed回调调用provider.toggleFavorite切换状态。这个操作会触发notifyListeners,所有监听AlbumProvider的Consumer都会重新构建,包括收藏页面。用户在详情页收藏照片后,返回收藏页面就能看到新收藏的照片,不需要下拉刷新或其他手动操作,体验很流畅。

性能优化

当收藏照片很多时,可以考虑分页加载:

return GridView.builder(
  itemCount: favorites.length,
  itemBuilder: (context, index) {
    // 按需构建
  },
);

GridView.builder只构建可见区域的item。

滚动时动态创建和销毁,内存占用可控。

如果要加载真实图片,还需要配合图片缓存。

GridView.builder的性能优势在于懒加载。itemBuilder只在item进入可见区域时才被调用,创建对应的Widget。滚动时,离开可见区域的item会被销毁,释放内存。这种机制让GridView能处理成千上万的item,内存占用始终保持在合理范围。如果用GridView.count配合children数组,所有item都会被创建,数据量大时会导致内存溢出。加载真实图片时,还需要配合图片缓存库,比如cached_network_image。这个库会把下载的图片缓存到本地,下次加载时直接从缓存读取,不需要重复下载,大大提升加载速度和用户体验。

用户体验细节

几个提升体验的小点:

Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey[300])

空状态图标够大,用户一眼就能看到。

颜色够浅,不会让页面显得很空洞。

空状态设计的关键是让用户知道当前状态,并引导下一步操作。图标尺寸64像素,在手机屏幕上很醒目,用户打开页面第一眼就能看到。颜色用grey[300],是个很浅的灰色,既能引起注意又不会太突兀。如果用深色图标,空状态页面会显得很沉重,给人一种"出错了"的感觉。浅色图标配合友好的提示文字,让用户知道这是正常状态,只是还没有收藏内容,心理上更容易接受。

Text('点击照片上的❤️可以收藏')

引导文字告诉用户下一步该做什么。

新用户不会迷茫,知道怎么开始收藏。

批量管理功能

添加批量取消收藏的功能:

class FavoritesScreen extends StatefulWidget {
  const FavoritesScreen({super.key});

  
  State<FavoritesScreen> createState() => _FavoritesScreenState();
}

class _FavoritesScreenState extends State<FavoritesScreen> {
  bool _isSelectionMode = false;
  final Set<String> _selectedPhotos = {};

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_isSelectionMode ? '已选择 ${_selectedPhotos.length} 项' : '我的收藏'),
        actions: [
          if (_isSelectionMode) ...[
            IconButton(
              icon: const Icon(Icons.select_all),
              onPressed: () {
                final provider = context.read<AlbumProvider>();
                setState(() {
                  if (_selectedPhotos.length == provider.favoritePhotos.length) {
                    _selectedPhotos.clear();
                  } else {
                    _selectedPhotos.addAll(
                      provider.favoritePhotos.map((p) => p.id),
                    );
                  }
                });
              },
            ),
            IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => _showBatchDeleteDialog(context),
            ),
            IconButton(
              icon: const Icon(Icons.close),
              onPressed: () {
                setState(() {
                  _isSelectionMode = false;
                  _selectedPhotos.clear();
                });
              },
            ),
          ] else
            IconButton(
              icon: const Icon(Icons.checklist),
              onPressed: () {
                setState(() {
                  _isSelectionMode = true;
                });
              },
            ),
        ],
      ),

批量管理模式用_isSelectionMode标记,_selectedPhotos存储选中的照片ID。进入选择模式后,AppBar标题显示选中数量,右侧显示全选、删除和取消按钮。全选按钮的逻辑是:如果已经全选了,点击后清空选择;如果没全选,点击后选中所有照片。这种切换逻辑很常见,用户体验好。取消按钮退出选择模式,清空选中状态。未进入选择模式时,显示一个清单图标,点击后进入选择模式。

      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final favorites = provider.favoritePhotos;

          if (favorites.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey[300]),
                  SizedBox(height: 16.h),
                  Text(
                    '暂无收藏的照片',
                    style: TextStyle(color: Colors.grey, fontSize: 16.sp),
                  ),
                  SizedBox(height: 8.h),
                  Text(
                    '点击照片上的❤️可以收藏',
                    style: TextStyle(color: Colors.grey[400], fontSize: 14.sp),
                  ),
                ],
              ),
            );
          }

          return GridView.builder(
            padding: EdgeInsets.all(16.w),
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              mainAxisSpacing: 4.w,
              crossAxisSpacing: 4.w,
            ),
            itemCount: favorites.length,
            itemBuilder: (context, index) {
              final photo = favorites[index];
              final isSelected = _selectedPhotos.contains(photo.id);
              
              return _buildPhotoCard(context, photo, isSelected);
            },
          );
        },
      ),
    );
  }

GridView的itemBuilder传入isSelected参数,表示当前照片是否被选中。这个状态会影响照片卡片的显示效果,选中的照片会有特殊的视觉反馈。

  Widget _buildPhotoCard(BuildContext context, PhotoModel photo, bool isSelected) {
    return InkWell(
      onTap: () {
        if (_isSelectionMode) {
          setState(() {
            if (isSelected) {
              _selectedPhotos.remove(photo.id);
            } else {
              _selectedPhotos.add(photo.id);
            }
          });
        } else {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => PhotoDetailScreen(photo: photo)),
          );
        }
      },
      onLongPress: () {
        if (!_isSelectionMode) {
          setState(() {
            _isSelectionMode = true;
            _selectedPhotos.add(photo.id);
          });
        }
      },
      child: Stack(
        fit: StackFit.expand,
        children: [
          Container(
            color: _getColorForPhoto(photo.url),
            child: Center(
              child: Icon(Icons.photo, color: Colors.white.withOpacity(0.5)),
            ),
          ),
          if (_isSelectionMode)
            Container(
              color: isSelected 
                  ? const Color(0xFFE91E63).withOpacity(0.3)
                  : Colors.black.withOpacity(0.1),
            ),
          if (_isSelectionMode)
            Positioned(
              top: 4.w,
              right: 4.w,
              child: Container(
                width: 24.w,
                height: 24.w,
                decoration: BoxDecoration(
                  color: isSelected ? const Color(0xFFE91E63) : Colors.white,
                  shape: BoxShape.circle,
                  border: Border.all(
                    color: isSelected ? const Color(0xFFE91E63) : Colors.grey,
                    width: 2,
                  ),
                ),
                child: isSelected
                    ? const Icon(Icons.check, color: Colors.white, size: 16)
                    : null,
              ),
            )
          else
            Positioned(
              top: 4.w,
              right: 4.w,
              child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
            ),
        ],
      ),
    );
  }

照片卡片的交互逻辑比较复杂。在选择模式下,点击照片切换选中状态;非选择模式下,点击跳转到详情页。长按照片进入选择模式,并选中当前照片。选择模式下,照片上方叠加一层半透明遮罩,选中的照片用粉色遮罩,未选中的用黑色遮罩。右上角显示选择框,选中的照片显示粉色圆圈加白色对勾,未选中的显示白色圆圈。非选择模式下,右上角显示红色爱心,表示已收藏。这种设计让用户能清楚地看到当前状态和可执行的操作。

  void _showBatchDeleteDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('批量取消收藏'),
        content: Text('确定要取消收藏选中的 ${_selectedPhotos.length} 张照片吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              final provider = context.read<AlbumProvider>();
              for (final photoId in _selectedPhotos) {
                provider.toggleFavorite(photoId);
              }
              Navigator.pop(context);
              setState(() {
                _isSelectionMode = false;
                _selectedPhotos.clear();
              });
            },
            child: const Text('确定', style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

  Color _getColorForPhoto(String url) {
    final colors = [
      Colors.pink[200]!,
      Colors.purple[200]!,
      Colors.blue[200]!,
      Colors.teal[200]!,
    ];
    return colors[url.hashCode % colors.length];
  }
}

批量删除对话框显示选中的照片数量,确认后遍历_selectedPhotos,对每张照片调用toggleFavorite取消收藏。操作完成后退出选择模式,清空选中状态。这种批量操作能显著提升效率,特别是需要清理大量收藏时。

收藏分类展示

按相册分类展示收藏的照片:

  Widget _buildCategoryView(List<PhotoModel> favorites) {
    final categoryMap = <String, List<PhotoModel>>{};
    
    for (final photo in favorites) {
      final category = photo.albumName ?? '未分类';
      categoryMap.putIfAbsent(category, () => []).add(photo);
    }
    
    return ListView.builder(
      padding: EdgeInsets.all(16.w),
      itemCount: categoryMap.length,
      itemBuilder: (context, index) {
        final category = categoryMap.keys.elementAt(index);
        final photos = categoryMap[category]!;
        
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: EdgeInsets.symmetric(vertical: 12.h),
              child: Row(
                children: [
                  Container(
                    width: 4.w,
                    height: 16.h,
                    decoration: BoxDecoration(
                      color: const Color(0xFFE91E63),
                      borderRadius: BorderRadius.circular(2.r),
                    ),
                  ),
                  SizedBox(width: 8.w),
                  Text(
                    category,
                    style: TextStyle(
                      fontSize: 16.sp,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  SizedBox(width: 8.w),
                  Text(
                    '${photos.length}张',
                    style: TextStyle(
                      fontSize: 14.sp,
                      color: Colors.grey,
                    ),
                  ),
                ],
              ),
            ),
            GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                mainAxisSpacing: 4.w,
                crossAxisSpacing: 4.w,
              ),
              itemCount: photos.length,
              itemBuilder: (context, photoIndex) {
                final photo = photos[photoIndex];
                return InkWell(
                  onTap: () => Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (_) => PhotoDetailScreen(photo: photo),
                    ),
                  ),
                  child: Stack(
                    fit: StackFit.expand,
                    children: [
                      Container(
                        color: _getColorForPhoto(photo.url),
                        child: Center(
                          child: Icon(
                            Icons.photo,
                            color: Colors.white.withOpacity(0.5),
                          ),
                        ),
                      ),
                      Positioned(
                        top: 4.w,
                        right: 4.w,
                        child: Icon(
                          Icons.favorite,
                          color: Colors.red,
                          size: 20.sp,
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
            SizedBox(height: 24.h),
          ],
        );
      },
    );
  }

分类视图先把收藏的照片按相册分组,用Map存储。遍历favorites,根据albumName分组,没有相册名的归入"未分类"。然后用ListView展示每个分类,每个分类包含标题和照片网格。标题左边有个粉色竖条装饰,显示分类名称和照片数量。照片网格用shrinkWrap和NeverScrollableScrollPhysics,让它不可滚动,高度自适应。这样整个页面只有外层ListView可以滚动,用户体验更好。这种分类展示能让用户快速找到特定相册的收藏照片。

收藏统计信息

在页面顶部显示收藏统计:

  Widget _buildStatistics(List<PhotoModel> favorites) {
    final categoryCount = <String, int>{};
    for (final photo in favorites) {
      final category = photo.albumName ?? '未分类';
      categoryCount[category] = (categoryCount[category] ?? 0) + 1;
    }
    
    return Container(
      margin: EdgeInsets.all(16.w),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: const Color(0xFFE91E63).withOpacity(0.1),
        borderRadius: BorderRadius.circular(12.r),
      ),
      child: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildStatItem('收藏总数', favorites.length.toString(), Icons.favorite),
              Container(width: 1, height: 40.h, color: Colors.grey[300]),
              _buildStatItem('相册数', categoryCount.length.toString(), Icons.photo_album),
            ],
          ),
          if (categoryCount.isNotEmpty) ...[
            SizedBox(height: 16.h),
            Divider(color: Colors.grey[300]),
            SizedBox(height: 12.h),
            Wrap(
              spacing: 8.w,
              runSpacing: 8.h,
              children: categoryCount.entries.map((entry) {
                return Container(
                  padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(16.r),
                  ),
                  child: Text(
                    '${entry.key} ${entry.value}',
                    style: TextStyle(fontSize: 12.sp),
                  ),
                );
              }).toList(),
            ),
          ],
        ],
      ),
    );
  }
  
  Widget _buildStatItem(String label, String value, IconData icon) {
    return Column(
      children: [
        Icon(icon, color: const Color(0xFFE91E63), size: 32),
        SizedBox(height: 8.h),
        Text(
          value,
          style: TextStyle(
            fontSize: 24.sp,
            fontWeight: FontWeight.bold,
            color: const Color(0xFFE91E63),
          ),
        ),
        SizedBox(height: 4.h),
        Text(
          label,
          style: TextStyle(
            fontSize: 12.sp,
            color: Colors.grey[600],
          ),
        ),
      ],
    );
  }

统计信息卡片显示收藏总数和涉及的相册数。下方用Wrap展示每个相册的收藏数量,每个标签显示相册名称和数量。这种统计信息能让用户对收藏情况有整体的了解,知道哪些相册的照片收藏得多。卡片用浅粉色背景,和主题色呼应。两个统计项用竖线分隔,视觉上更清晰。

小结

收藏页面虽然简单,但空状态处理和视觉细节很重要。

Consumer监听数据变化,Stack叠加收藏标记。

引导文字帮助新用户快速上手。

批量管理功能让用户能高效地管理收藏,长按进入选择模式,支持全选和批量取消收藏。分类展示让用户能按相册查看收藏的照片,统计信息提供整体概览。这些功能的组合让收藏页面不仅是简单的列表展示,而是一个功能完整的收藏管理工具。


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

Logo

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

更多推荐