在这里插入图片描述

手办收藏爱好者都知道,不同厂商的产品风格、品质、价格定位各不相同。Good Smile Company 以粘土人系列闻名,Alter 则以高端比例手办著称,Bandai 的万代系列覆盖众多动漫 IP。因此,在手办收藏应用中,厂商列表页面是帮助用户快速筛选、浏览特定品牌产品的关键功能。本文将深入剖析如何在 Flutter for OpenHarmony 项目中实现一个实用的厂商列表页面。

一、厂商列表的业务价值

在手办收藏场景中,厂商列表不仅是简单的品牌展示,更承载着以下核心功能:

  • 品牌筛选:用户可以快速找到自己喜欢的厂商,查看该厂商的所有产品
  • 产品数量统计:直观展示每个厂商的产品规模,帮助用户了解品牌活跃度
  • 跳转详情:点击厂商后进入专属页面,查看该厂商的详细介绍、热门产品等
  • 收藏管理:用户可以关注特定厂商,当该厂商发布新品时收到通知

这些需求决定了厂商列表页面需要信息密度适中、交互流畅、扩展性强

二、页面架构设计

1. 无状态组件的选择

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

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

采用 StatelessWidget 是因为厂商数据相对稳定,不需要频繁更新。即使后续需要添加搜索、排序等功能,也可以通过 Provider 或 GetX 进行状态管理,而非在页面内部维护状态。这种设计符合 Flutter 的最佳实践,保持组件职责单一。

2. 数据源定义

  
  Widget build(BuildContext context) {
    final manufacturers = ['Good Smile Company', 'Kotobukiya', 'Alter', 'Max Factory', 'Bandai', 'Aniplex'];

当前使用硬编码数组模拟数据,实际项目中应从以下来源获取:

  • 本地数据库:使用 sqflite 存储厂商信息,支持离线访问
  • 远程 API:从服务器获取最新的厂商列表,包含 logo、简介等详细信息
  • 混合模式:本地缓存 + 远程更新,兼顾速度与实时性

这里选择的 6 个厂商都是手办行业的知名品牌,覆盖了不同价位和风格,具有代表性。

三、列表渲染的核心逻辑

1. Scaffold 基础结构

    return Scaffold(
      appBar: AppBar(title: const Text('厂商列表')),

顶部导航栏显示"厂商列表"标题,让用户明确当前所在页面。在完整版本中,可以在 AppBar 右侧添加搜索图标,点击后展开搜索框,支持按厂商名称快速定位。

2. ListView.builder 高效渲染

      body: ListView.builder(
        itemCount: manufacturers.length,
        itemBuilder: (context, index) {

为什么选择 builder 模式?

  • 性能优化:只渲染可见区域的列表项,当厂商数量达到几十个甚至上百个时,性能优势明显
  • 动态扩展:厂商数量不固定,builder 能灵活适应数据变化
  • 内存友好:在 OpenHarmony 设备上,避免一次性创建大量 Widget 导致内存峰值

3. Card 卡片样式

          return Card(
            margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),

使用 Card 组件的优势:

  • 视觉分隔:自带阴影效果,让每个厂商条目独立清晰
  • 点击反馈:Card 内部的 ListTile 自带水波纹效果,提升交互体验
  • 间距控制:通过 margin 设置水平 16 像素、垂直 8 像素的间距,避免列表过于紧凑

这里的 .w.h 是 ScreenUtil 提供的响应式单位,确保在不同屏幕尺寸的 OpenHarmony 设备上都能保持合适的间距比例。

四、ListTile 内容布局详解

1. leading 头像设计

            child: ListTile(
              leading: CircleAvatar(
                child: Text(manufacturers[index][0]),
              ),

CircleAvatar 的妙用

  • 显示厂商名称的首字母,如"G"代表 Good Smile Company
  • 当没有厂商 logo 图片时,首字母是很好的替代方案
  • 圆形头像符合现代 UI 设计规范,视觉上更柔和

在实际项目中,可以这样优化:

leading: CircleAvatar(
  backgroundImage: manufacturer.logoUrl != null 
    ? NetworkImage(manufacturer.logoUrl!) 
    : null,
  child: manufacturer.logoUrl == null 
    ? Text(manufacturer.name[0]) 
    : null,
),

优先显示真实 logo,无图片时才显示首字母。

2. title 厂商名称

              title: Text(manufacturers[index]),

直接显示厂商全称,字体大小使用系统默认值(通常为 16sp),确保可读性。如果厂商名称过长(如"Good Smile Company"),ListTile 会自动处理文本溢出,显示省略号。

3. subtitle 产品数量统计

              subtitle: Text('产品数量: ${(index + 1) * 100}'),

数量计算逻辑

  • 当前使用 (index + 1) * 100 模拟数据,第一个厂商显示 100,第二个显示 200,以此类推
  • 实际项目中应从数据库查询:SELECT COUNT(*) FROM figures WHERE manufacturer_id = ?
  • 这个数字能帮助用户快速判断厂商的产品丰富度

可以进一步优化显示格式:

subtitle: Text('产品数量: ${manufacturer.productCount} | 关注: ${manufacturer.followersCount}'),

同时显示产品数量和关注人数,信息更全面。

4. trailing 箭头图标

              trailing: const Icon(Icons.arrow_forward_ios, size: 16),

右侧箭头的作用

  • 视觉提示:告诉用户这是一个可点击的条目
  • 统一规范:符合移动端列表页的通用设计模式
  • 尺寸控制:16 像素的箭头不会过于显眼,保持页面平衡

5. onTap 点击事件

              onTap: () {},

当前为空实现,实际项目中应跳转到厂商详情页:

onTap: () {
  Get.to(() => ManufacturerDetailPage(
    manufacturerId: manufacturers[index].id,
    manufacturerName: manufacturers[index],
  ));
},

使用 GetX 的路由管理,传递厂商 ID 和名称参数。

五、数据模型设计

为了支持更复杂的业务逻辑,应定义厂商数据模型:

class Manufacturer {
  final String id;
  final String name;
  final String? logoUrl;
  final int productCount;
  final int followersCount;
  final String description;
  final String country;
  
  Manufacturer({
    required this.id,
    required this.name,
    this.logoUrl,
    required this.productCount,
    required this.followersCount,
    required this.description,
    required this.country,
  });
  
  factory Manufacturer.fromJson(Map<String, dynamic> json) {
    return Manufacturer(
      id: json['id'],
      name: json['name'],
      logoUrl: json['logo_url'],
      productCount: json['product_count'],
      followersCount: json['followers_count'],
      description: json['description'],
      country: json['country'],
    );
  }
}

这样可以存储更多厂商信息,如国家、简介等,为详情页提供数据支持。

六、状态管理集成

1. 创建 ManufacturerProvider

class ManufacturerProvider extends ChangeNotifier {
  List<Manufacturer> _manufacturers = [];
  bool _isLoading = false;
  
  List<Manufacturer> get manufacturers => _manufacturers;
  bool get isLoading => _isLoading;
  
  Future<void> fetchManufacturers() async {
    _isLoading = true;
    notifyListeners();
    
    try {
      final response = await http.get(Uri.parse('$apiBaseUrl/manufacturers'));
      final List<dynamic> data = json.decode(response.body);
      _manufacturers = data.map((json) => Manufacturer.fromJson(json)).toList();
    } catch (e) {
      print('加载厂商列表失败: $e');
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
  
  List<Manufacturer> searchManufacturers(String keyword) {
    return _manufacturers.where((m) => 
      m.name.toLowerCase().contains(keyword.toLowerCase())
    ).toList();
  }
}

通过 Provider 管理厂商数据,支持加载、搜索等操作。

2. 页面改造

body: Consumer<ManufacturerProvider>(
  builder: (context, provider, child) {
    if (provider.isLoading) {
      return Center(child: CircularProgressIndicator());
    }
    
    if (provider.manufacturers.isEmpty) {
      return Center(child: Text('暂无厂商数据'));
    }
    
    return ListView.builder(
      itemCount: provider.manufacturers.length,
      itemBuilder: (context, index) {
        final manufacturer = provider.manufacturers[index];
        return Card(
          // 使用真实数据渲染
        );
      },
    );
  },
),

通过 Consumer 监听数据变化,自动刷新列表。

七、搜索功能实现

在 AppBar 中添加搜索框:

appBar: AppBar(
  title: _isSearching 
    ? TextField(
        autofocus: true,
        decoration: InputDecoration(
          hintText: '搜索厂商...',
          border: InputBorder.none,
        ),
        onChanged: (value) {
          provider.searchManufacturers(value);
        },
      )
    : Text('厂商列表'),
  actions: [
    IconButton(
      icon: Icon(_isSearching ? Icons.close : Icons.search),
      onPressed: () {
        setState(() {
          _isSearching = !_isSearching;
        });
      },
    ),
  ],
),

点击搜索图标后,标题栏变为输入框,实时过滤厂商列表。

八、OpenHarmony 适配要点

1. 屏幕适配

使用 ScreenUtil 确保在不同设备上显示一致:

ScreenUtil.init(
  context,
  designSize: Size(375, 812),
  minTextAdapt: true,
);

设计稿基于 375x812 尺寸,ScreenUtil 会自动缩放。

2. 网络请求优化

OpenHarmony 设备可能处于弱网环境,需要添加超时和重试机制:

final response = await http.get(
  Uri.parse('$apiBaseUrl/manufacturers'),
).timeout(Duration(seconds: 10), onTimeout: () {
  throw TimeoutException('请求超时');
});

超时后提示用户重试,避免长时间等待。

九、高级功能扩展

1. 厂商详情页跳转

实现完整的跳转逻辑:

onTap: () {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => ManufacturerDetailPage(
        manufacturer: manufacturers[index],
      ),
    ),
  );
}

在详情页中展示厂商的完整信息:

class ManufacturerDetailPage extends StatelessWidget {
  final Manufacturer manufacturer;
  
  const ManufacturerDetailPage({required this.manufacturer});
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(manufacturer.name)),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (manufacturer.logoUrl != null)
              Center(
                child: Image.network(
                  manufacturer.logoUrl!,
                  height: 100.h,
                ),
              ),
            SizedBox(height: 16.h),
            Text(
              manufacturer.description,
              style: TextStyle(fontSize: 14.sp),
            ),
            SizedBox(height: 16.h),
            Text(
              '产品数量: ${manufacturer.productCount}',
              style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
            ),
          ],
        ),
      ),
    );
  }
}

2. 排序功能

添加按产品数量、关注人数排序:

enum SortType { name, productCount, followersCount }

class ManufacturerProvider extends ChangeNotifier {
  SortType _sortType = SortType.name;
  
  void setSortType(SortType type) {
    _sortType = type;
    _sortManufacturers();
    notifyListeners();
  }
  
  void _sortManufacturers() {
    switch (_sortType) {
      case SortType.name:
        _manufacturers.sort((a, b) => a.name.compareTo(b.name));
        break;
      case SortType.productCount:
        _manufacturers.sort((a, b) => b.productCount.compareTo(a.productCount));
        break;
      case SortType.followersCount:
        _manufacturers.sort((a, b) => b.followersCount.compareTo(a.followersCount));
        break;
    }
  }
}

在 AppBar 添加排序菜单:

actions: [
  PopupMenuButton<SortType>(
    onSelected: (type) => provider.setSortType(type),
    itemBuilder: (context) => [
      PopupMenuItem(
        value: SortType.name,
        child: Text('按名称'),
      ),
      PopupMenuItem(
        value: SortType.productCount,
        child: Text('按产品数量'),
      ),
      PopupMenuItem(
        value: SortType.followersCount,
        child: Text('按关注人数'),
      ),
    ],
  ),
],

3. 关注功能

实现厂商关注/取消关注:

class ManufacturerProvider extends ChangeNotifier {
  Set<String> _followedManufacturers = {};
  
  bool isFollowed(String manufacturerId) {
    return _followedManufacturers.contains(manufacturerId);
  }
  
  Future<void> toggleFollow(String manufacturerId) async {
    if (_followedManufacturers.contains(manufacturerId)) {
      _followedManufacturers.remove(manufacturerId);
      await _unfollowManufacturer(manufacturerId);
    } else {
      _followedManufacturers.add(manufacturerId);
      await _followManufacturer(manufacturerId);
    }
    notifyListeners();
  }
  
  Future<void> _followManufacturer(String id) async {
    await http.post(Uri.parse('$apiBaseUrl/manufacturers/$id/follow'));
  }
  
  Future<void> _unfollowManufacturer(String id) async {
    await http.delete(Uri.parse('$apiBaseUrl/manufacturers/$id/follow'));
  }
}

在列表项中添加关注按钮:

ListTile(
  leading: CircleAvatar(child: Text(manufacturer.name[0])),
  title: Text(manufacturer.name),
  subtitle: Text('产品数量: ${manufacturer.productCount}'),
  trailing: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      IconButton(
        icon: Icon(
          provider.isFollowed(manufacturer.id) 
            ? Icons.favorite 
            : Icons.favorite_border,
          color: provider.isFollowed(manufacturer.id) 
            ? Colors.red 
            : Colors.grey,
        ),
        onPressed: () => provider.toggleFollow(manufacturer.id),
      ),
      Icon(Icons.arrow_forward_ios, size: 16),
    ],
  ),
  onTap: () => _navigateToDetail(manufacturer),
)

4. 下拉刷新

添加下拉刷新功能:

RefreshIndicator(
  onRefresh: () async {
    await provider.fetchManufacturers();
  },
  child: ListView.builder(
    itemCount: provider.manufacturers.length,
    itemBuilder: (context, index) {
      // 列表项
    },
  ),
)

5. 分组显示

按国家或地区分组显示厂商:

Map<String, List<Manufacturer>> groupByCountry(List<Manufacturer> manufacturers) {
  final grouped = <String, List<Manufacturer>>{};
  for (var manufacturer in manufacturers) {
    if (!grouped.containsKey(manufacturer.country)) {
      grouped[manufacturer.country] = [];
    }
    grouped[manufacturer.country]!.add(manufacturer);
  }
  return grouped;
}

使用分组数据渲染列表:

ListView(
  children: groupedManufacturers.entries.map((entry) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: EdgeInsets.all(16.w),
          child: Text(
            entry.key,
            style: TextStyle(
              fontSize: 18.sp,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        ...entry.value.map((manufacturer) => _buildManufacturerTile(manufacturer)),
      ],
    );
  }).toList(),
)

十、性能优化技巧

1. 图片缓存

使用 CachedNetworkImage 缓存厂商 logo:

leading: CachedNetworkImage(
  imageUrl: manufacturer.logoUrl ?? '',
  imageBuilder: (context, imageProvider) => CircleAvatar(
    backgroundImage: imageProvider,
  ),
  placeholder: (context, url) => CircleAvatar(
    child: CircularProgressIndicator(),
  ),
  errorWidget: (context, url, error) => CircleAvatar(
    child: Text(manufacturer.name[0]),
  ),
)

2. 懒加载

实现分页加载,避免一次性加载过多数据:

class ManufacturerProvider extends ChangeNotifier {
  int _page = 1;
  bool _hasMore = true;
  
  Future<void> loadMore() async {
    if (!_hasMore || _isLoading) return;
    
    _isLoading = true;
    notifyListeners();
    
    try {
      final response = await http.get(
        Uri.parse('$apiBaseUrl/manufacturers?page=$_page&limit=20'),
      );
      final List<dynamic> data = json.decode(response.body);
      
      if (data.isEmpty) {
        _hasMore = false;
      } else {
        _manufacturers.addAll(
          data.map((json) => Manufacturer.fromJson(json)).toList(),
        );
        _page++;
      }
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

监听滚动位置,触底时加载更多:

ScrollController _scrollController = ScrollController();


void initState() {
  super.initState();
  _scrollController.addListener(() {
    if (_scrollController.position.pixels >= 
        _scrollController.position.maxScrollExtent - 200) {
      provider.loadMore();
    }
  });
}

3. 本地缓存

使用 sqflite 缓存厂商数据,支持离线访问:

Future<void> cacheManufacturers(List<Manufacturer> manufacturers) async {
  final db = await database;
  await db.delete('manufacturers');
  
  for (var manufacturer in manufacturers) {
    await db.insert('manufacturers', manufacturer.toJson());
  }
}

Future<List<Manufacturer>> loadCachedManufacturers() async {
  final db = await database;
  final maps = await db.query('manufacturers');
  return maps.map((map) => Manufacturer.fromJson(map)).toList();
}

十一、实战经验总结

厂商列表页面的实现要点:

  • 数据结构:定义完善的 Manufacturer 模型,支持扩展
  • 性能优化:使用 ListView.builder 按需渲染,避免卡顿
  • 交互设计:点击跳转、搜索过滤等功能提升用户体验
  • 状态管理:通过 Provider 统一管理数据,降低耦合度
  • 高级功能:排序、关注、分组等功能提升用户体验
  • 性能优化:图片缓存、懒加载、本地缓存确保流畅运行

通过本文的实战讲解,你已经掌握了在 Flutter for OpenHarmony 项目中构建厂商列表的核心技巧。在后续开发中,可以继续扩展排序、筛选、关注等功能,让厂商列表成为用户探索手办世界的重要入口。


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

Logo

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

更多推荐