在这里插入图片描述

发现社团是用户探索和加入新社团的主要入口,可以按分类浏览所有社团。这篇文章带大家实现发现社团模块。

页面结构搭建

发现社团页面需要管理TabController,用StatefulWidget来实现:

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

  
  State<DiscoverPage> createState() => _DiscoverPageState();
}

StatefulWidget允许我们在页面内部维护可变状态。
const构造函数可以让Flutter在重建时复用Widget实例。

导入依赖包

在文件开头导入需要的包:

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

material.dart提供Material Design风格的组件。
provider用于状态管理。

还需要导入项目内的文件:

import '../../providers/app_provider.dart';
import '../../models/club.dart';
import '../club/club_detail_page.dart';
import '../club/club_ranking_page.dart';
import '../club/club_category_page.dart';

这些是项目内部的页面和模型文件。
使用相对路径引入,…/…/表示向上两级目录。

状态变量定义

定义TabController和分类列表:

class _DiscoverPageState extends State<DiscoverPage> 
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  final List<String> _categories = [
    '全部', '科技', '艺术', '体育', '学术', '公益', '文娱'
  ];

SingleTickerProviderStateMixin为TabController提供vsync参数。
分类列表包含全部和六个具体分类。

生命周期管理

在initState和dispose中管理TabController:

  
  void initState() {
    super.initState();
    _tabController = TabController(
      length: _categories.length, 
      vsync: this
    );
  }

initState中创建TabController,length参数指定Tab数量。
super.initState()必须首先调用。

释放资源:

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

dispose中释放TabController,避免内存泄漏。
这是Flutter开发中的最佳实践。

构建AppBar

AppBar包含标题、操作按钮和TabBar:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('发现社团'),
        actions: [

Scaffold是Material Design的基础布局组件。
actions数组放置AppBar右侧的操作按钮。

排行榜按钮:

          IconButton(
            icon: const Icon(Icons.leaderboard),
            onPressed: () => Navigator.push(
              context, 
              MaterialPageRoute(
                builder: (_) => const ClubRankingPage()
              )
            ),
          ),

leaderboard图标表示排行榜功能。
点击跳转到社团排行榜页面。

分类按钮:

          IconButton(
            icon: const Icon(Icons.category),
            onPressed: () => Navigator.push(
              context, 
              MaterialPageRoute(
                builder: (_) => const ClubCategoryPage()
              )
            ),
          ),
        ],

category图标表示分类功能。
两个快捷入口满足不同的浏览需求。

配置TabBar

TabBar放在AppBar的bottom位置:

        bottom: TabBar(
          controller: _tabController,
          isScrollable: true,
          indicatorColor: Colors.white,
          tabs: _categories.map((c) => Tab(text: c)).toList(),
        ),
      ),

isScrollable设为true让TabBar可以横向滚动。
白色指示器在蓝色背景上更醒目。

内容区域

使用Consumer监听AppProvider的数据变化:

      body: Consumer<AppProvider>(
        builder: (context, provider, _) {
          return TabBarView(
            controller: _tabController,

Consumer会在数据变化时自动重建子组件。
TabBarView和TabBar共用controller保证同步。

根据分类筛选社团:

            children: _categories.map((category) {
              final clubs = category == '全部'
                  ? provider.clubs
                  : provider.clubs
                      .where((c) => c.category == category)
                      .toList();
              return _buildClubList(clubs);
            }).toList(),
          );
        },
      ),
    );
  }

全部分类显示所有社团,其他分类根据category字段筛选。
map方法将每个分类转换为对应的列表视图。

社团列表构建

定义构建社团列表的方法:

  Widget _buildClubList(List<Club> clubs) {
    if (clubs.isEmpty) {
      return const Center(
        child: Text(
          '暂无社团', 
          style: TextStyle(color: Colors.grey)
        )
      );
    }

空状态时显示友好的提示文字。
灰色文字不会太突兀。

使用ListView.builder构建列表:

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: clubs.length,
      itemBuilder: (context, index) {
        final club = clubs[index];

ListView.builder是懒加载列表,性能更好。
padding设置16像素内边距。

社团卡片设计

每个社团用Card组件包裹:

        return Card(
          margin: const EdgeInsets.only(bottom: 12),
          child: InkWell(
            onTap: () => Navigator.push(
              context, 
              MaterialPageRoute(
                builder: (_) => ClubDetailPage(club: club)
              )
            ),

Card自带阴影和圆角。
InkWell提供水波纹点击效果。

卡片内容区:

            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [

Padding设置16像素内边距。
Row横向排列头像和信息区。

社团头像

左侧显示社团头像:

                  CircleAvatar(
                    radius: 30,
                    backgroundColor: const Color(0xFF4A90E2)
                        .withOpacity(0.1),
                    child: Text(
                      club.name[0], 
                      style: const TextStyle(
                        color: Color(0xFF4A90E2), 
                        fontSize: 24, 
                        fontWeight: FontWeight.bold
                      )
                    ),
                  ),
                  const SizedBox(width: 16),

CircleAvatar显示社团名称的首字母。
蓝色配色和整体风格保持一致。

社团名称和已加入标签

显示社团名称,如果已加入则显示标签:

                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Expanded(
                              child: Text(
                                club.name, 
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold, 
                                  fontSize: 16
                                )
                              )
                            ),

社团名称用粗体16像素字号。
Expanded让名称占据剩余空间。

已加入标签:

                            if (club.isJoined)
                              Container(
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 8, 
                                  vertical: 2
                                ),
                                decoration: BoxDecoration(
                                  color: Colors.green.withOpacity(0.1),
                                  borderRadius: BorderRadius.circular(10),
                                ),
                                child: const Text(
                                  '已加入', 
                                  style: TextStyle(
                                    color: Colors.green, 
                                    fontSize: 12
                                  )
                                ),
                              ),
                          ],
                        ),

已加入标签用绿色,和其他状态标签区分。
条件渲染只在已加入时显示。

社团简介

显示社团简介:

                        const SizedBox(height: 4),
                        Text(
                          club.description, 
                          maxLines: 2, 
                          overflow: TextOverflow.ellipsis, 
                          style: const TextStyle(
                            color: Colors.grey, 
                            fontSize: 13
                          )
                        ),

简介限制两行显示,超出部分用省略号。
灰色小字作为辅助信息。

分类和统计信息

显示分类标签、成员数和评分:

                        const SizedBox(height: 8),
                        Row(
                          children: [
                            Container(
                              padding: const EdgeInsets.symmetric(
                                horizontal: 6, 
                                vertical: 2
                              ),
                              decoration: BoxDecoration(
                                color: const Color(0xFF4A90E2)
                                    .withOpacity(0.1),
                                borderRadius: BorderRadius.circular(4),
                              ),
                              child: Text(
                                club.category, 
                                style: const TextStyle(
                                  color: Color(0xFF4A90E2), 
                                  fontSize: 11
                                )
                              ),
                            ),

分类标签用蓝色背景,和整体风格保持一致。
圆角设为4像素。

成员数和评分:

                            const SizedBox(width: 8),
                            const Icon(
                              Icons.people, 
                              size: 14, 
                              color: Colors.grey
                            ),
                            const SizedBox(width: 4),
                            Text(
                              '${club.memberCount}人', 
                              style: const TextStyle(
                                fontSize: 12, 
                                color: Colors.grey
                              )
                            ),
                            const SizedBox(width: 12),
                            const Icon(
                              Icons.star, 
                              size: 14, 
                              color: Colors.amber
                            ),
                            const SizedBox(width: 4),
                            Text(
                              club.rating.toStringAsFixed(1), 
                              style: const TextStyle(
                                fontSize: 12, 
                                color: Colors.grey
                              )
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

成员数用people图标,评分用amber色的star图标。
toStringAsFixed(1)保留一位小数。

TabBar可滚动的设计

当分类数量较多时,TabBar设置为可滚动是更好的选择。
这样可以容纳更多的分类,用户通过左右滑动查看所有分类。

如果强制一行显示,每个Tab会变得很窄,文字可能显示不全。
可滚动的TabBar在移动端是常见的设计模式。

社团卡片的信息密度

社团卡片在有限空间内展示了丰富的信息。
包括头像、名称、已加入状态、简介、分类、成员数、评分等。

这些信息帮助用户快速了解社团的基本情况。
用户可以决定是否要查看详情或申请加入。

小结

发现社团页面通过TabBar实现分类浏览功能。用户可以按照全部、科技、艺术等分类筛选社团。社团卡片展示头像、名称、简介、分类、成员数、评分等信息,已加入的社团显示绿色标签。点击卡片可以进入社团详情页面。


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐