Flutter for OpenHarmony 看书管理记录App实战:书籍列表实现
本文介绍了如何实现一个书籍列表页面,采用网格布局展示书籍封面。页面使用TabBar实现四种状态筛选(全部/在读/已读/想读),包含搜索和添加功能。网格布局每行显示两本书,卡片分为封面区域和信息区域,封面区域占3/5高度并显示书籍图标。右下角浮动按钮可跳转添加新书,点击书籍卡片可查看详情。整体采用深棕色主题配色,确保界面清晰易用。

书籍列表是整个App的核心页面之一,用户可以在这里查看自己所有的书籍,按状态筛选,还能快速添加新书。今天来实现这个页面,主要涉及TabBar切换和网格布局。
做书籍列表的时候,我考虑了几种展示方式:列表、网格、书架。最后选了网格布局,因为书籍封面是竖向的,网格布局能更好地展示封面,视觉效果也更像真实的书架。
页面整体结构
书籍列表用 StatefulWidget 实现,因为需要管理 TabController 的状态。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../app/routes/app_routes.dart';
class BookListPage extends StatefulWidget {
const BookListPage({super.key});
State<BookListPage> createState() => _BookListPageState();
}
导入必要的依赖,StatefulWidget 是因为 TabController 需要 TickerProvider,必须用有状态组件。
状态类定义
class _BookListPageState extends State<BookListPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<String> _tabs = ['全部', '在读', '已读', '想读'];
void initState() {
super.initState();
_tabController = TabController(length: _tabs.length, vsync: this);
}
void dispose() {
_tabController.dispose();
super.dispose();
}
SingleTickerProviderStateMixin 提供动画所需的 Ticker。TabController 在 initState 中初始化,在 dispose 中销毁,这是标准的生命周期管理。
四个Tab分别是:全部、在读、已读、想读。这样用户可以快速筛选不同状态的书籍。
AppBar 和 TabBar
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFDF8F3),
appBar: AppBar(
title: const Text('我的书籍'),
backgroundColor: const Color(0xFF5B4636),
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
actions: [
IconButton(icon: const Icon(Icons.filter_list), onPressed: () {}),
IconButton(icon: const Icon(Icons.search), onPressed: () => Get.toNamed(AppRoutes.search)),
],
automaticallyImplyLeading: false 是因为这个页面是底部导航的一个Tab,不需要返回按钮。右上角放了筛选和搜索两个按钮。
TabBar 样式
bottom: TabBar(
controller: _tabController,
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
tabs: _tabs.map((t) => Tab(text: t)).toList(),
),
),
TabBar 放在 AppBar 的 bottom 位置,指示器和选中文字都是白色,未选中的文字是半透明白色。这样和深棕色背景形成对比,看起来很清晰。
TabBarView 内容
body: TabBarView(
controller: _tabController,
children: _tabs.map((t) => _buildBookGrid()).toList(),
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFF5B4636),
onPressed: () => Get.toNamed(AppRoutes.addBook),
child: const Icon(Icons.add, color: Colors.white),
),
);
}
每个Tab对应一个书籍网格,目前都用同一个方法生成,实际项目中应该根据Tab筛选不同状态的书籍。
右下角的 FloatingActionButton 用来添加新书,点击跳转到添加书籍页面。
书籍数据
Widget _buildBookGrid() {
final books = [
{'title': '百年孤独', 'author': '加西亚·马尔克斯', 'status': '在读', 'rating': 4.5},
{'title': '人类简史', 'author': '尤瓦尔·赫拉利', 'status': '在读', 'rating': 4.8},
{'title': '三体', 'author': '刘慈欣', 'status': '已读', 'rating': 5.0},
{'title': '活着', 'author': '余华', 'status': '已读', 'rating': 4.9},
{'title': '小王子', 'author': '圣埃克苏佩里', 'status': '想读', 'rating': 4.7},
{'title': '1984', 'author': '乔治·奥威尔', 'status': '想读', 'rating': 4.6},
];
书籍数据暂时写死,包含书名、作者、状态、评分四个字段。实际项目中这些数据应该从数据库查询。
GridView 配置
return GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16.h,
crossAxisSpacing: 16.w,
childAspectRatio: 0.65,
),
itemCount: books.length,
itemBuilder: (context, index) {
final book = books[index];
return GestureDetector(
onTap: () => Get.toNamed(AppRoutes.bookDetail),
GridView.builder 比 GridView.count 性能更好,因为它是懒加载的。crossAxisCount: 2 表示每行两本书,childAspectRatio: 0.65 控制卡片的宽高比,让它更接近书籍封面的比例。
书籍卡片容器
child: Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF5B4636).withOpacity(0.1),
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
child: Center(child: Icon(Icons.menu_book, size: 48.sp, color: const Color(0xFF5B4636))),
),
),
卡片分为上下两部分,上面是封面区域(占3份),下面是信息区域(占2份)。封面区域用浅棕色背景加书籍图标,实际项目中应该显示真实的封面图片。
BorderRadius.vertical(top: ...) 只给顶部加圆角,底部是直角,这样和下面的信息区域衔接更自然。
书籍信息区域
Expanded(
flex: 2,
child: Padding(
padding: EdgeInsets.all(10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book['title'] as String, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14.sp), maxLines: 1, overflow: TextOverflow.ellipsis),
SizedBox(height: 2.h),
Text(book['author'] as String, style: TextStyle(color: Colors.grey[600], fontSize: 11.sp), maxLines: 1),
const Spacer(),
书名用加粗字体,作者用灰色小字。maxLines: 1 配合 overflow: TextOverflow.ellipsis 处理文字过长的情况。
Spacer() 把评分和状态标签推到底部,保持布局一致。
评分和状态标签
Row(
children: [
Icon(Icons.star, color: Colors.amber, size: 14.sp),
SizedBox(width: 2.w),
Text('${book['rating']}', style: TextStyle(fontSize: 12.sp, color: Colors.grey[700])),
const Spacer(),
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(color: const Color(0xFF5B4636).withOpacity(0.1), borderRadius: BorderRadius.circular(4.r)),
child: Text(book['status'] as String, style: TextStyle(fontSize: 10.sp, color: const Color(0xFF5B4636))),
),
],
),
],
),
),
),
],
),
),
);
},
);
}
}
左边是评分,用星星图标加数字。右边是状态标签,用浅棕色背景的小标签显示。这样用户一眼就能看到每本书的状态。
为什么用网格布局
选择网格布局而不是列表布局,主要有几个原因:
书籍封面通常是竖向的,网格布局能更好地展示封面比例。如果用列表布局,要么封面很小,要么每个item占用太多高度。
网格布局更像真实的书架,用户看起来更直观。两列的布局在手机上刚好,既不会太挤也不会太空。
网格布局的信息密度更高,一屏能看到更多书籍,方便用户快速浏览和查找。
Tab 切换逻辑
目前四个Tab都显示同样的内容,实际项目中应该根据Tab筛选:
// 示例:根据状态筛选书籍
// final filteredBooks = books.where((b) =>
// _tabs[_tabController.index] == '全部' ||
// b['status'] == _tabs[_tabController.index]
// ).toList();
这个逻辑后面接入数据库的时候再实现,现在先把UI做出来。
添加书籍入口
右下角的 FloatingActionButton 是添加书籍的主要入口。选择 FAB 而不是 AppBar 按钮,是因为:
FAB 更醒目,用户一眼就能看到。添加书籍是高频操作,应该放在容易触达的位置。
FAB 在右下角,用户单手操作时拇指刚好能够到,符合人体工程学。
Material Design 规范推荐用 FAB 来放置页面的主要操作。
性能优化
GridView.builder 是懒加载的,只会渲染当前可见的item,对于书籍很多的情况性能更好。
如果书籍数量特别多(比如上千本),还可以考虑分页加载,每次只加载一部分数据。
小结
书籍列表页面的核心是 TabBar + GridView 的组合。TabBar 提供状态筛选,GridView 提供网格展示。代码组织上,把网格构建抽成独立方法,主 build 方法保持简洁。
下一篇会讲书籍详情页面的实现,涉及到 SliverAppBar 和更复杂的布局,敬请期待。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)