Flutter for OpenHarmony 万能游戏库App实战 - 瑞克和莫蒂角色列表实现
本文介绍了如何实现《瑞克和莫蒂》动画角色列表页面,包含网格布局、状态筛选、搜索和无限滚动功能。页面采用Tab架构,使用AutomaticKeepAliveClientMixin保持状态。核心功能包括:通过ScrollController监听滚动实现无限加载;支持按存活状态筛选和关键词搜索;为不同状态角色显示不同颜色标识。实现过程中注重资源管理(如TabController释放)和用户体验优化(如滚
瑞克和莫蒂是一部经典的科幻喜剧动画,拥有众多有趣的角色。这篇文章我们来实现一个完整的角色列表页面,包括网格布局、状态筛选、搜索功能、以及无限滚动加载。这个功能展示了如何构建一个功能丰富的内容浏览界面。
页面的整体架构
RickMortyScreen是一个Tab页面,包含三个子页面:角色、地点、剧集。我们先看主页面的结构:
class RickMortyScreen extends StatefulWidget {
const RickMortyScreen({super.key});
State<RickMortyScreen> createState() => _RickMortyScreenState();
}
class _RickMortyScreenState extends State<RickMortyScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
void dispose() {
_tabController.dispose();
super.dispose();
}
使用SingleTickerProviderStateMixin来提供TabController所需的TickerProvider。
在dispose中释放TabController,这是重要的资源管理。
角色列表页面
CharactersTab是一个有状态的Widget,需要管理多个状态:
class CharactersTab extends StatefulWidget {
const CharactersTab({super.key});
State<CharactersTab> createState() => _CharactersTabState();
}
class _CharactersTabState extends State<CharactersTab> with AutomaticKeepAliveClientMixin {
final RickMortyApi _api = RickMortyApi();
final ScrollController _scrollController = ScrollController();
List<dynamic> _characters = [];
bool _isLoading = true;
bool _isLoadingMore = false;
int _page = 1;
int _totalPages = 1;
String? _statusFilter;
String? _searchQuery;
bool get wantKeepAlive => true;
使用AutomaticKeepAliveClientMixin来保持Tab页面的状态。这样当用户切换Tab后再切回来时,页面不会重新加载。
_statusFilter用来存储用户选择的状态筛选(存活、死亡、未知)。
_searchQuery用来存储用户输入的搜索关键词。
初始化和滚动监听
initState中初始化数据和添加滚动监听:
void initState() {
super.initState();
_loadCharacters();
_scrollController.addListener(_onScroll);
}
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
当用户滚动到距离底部200像素时,自动加载更多角色。
数据加载
_loadCharacters方法加载初始数据:
Future<void> _loadCharacters() async {
setState(() {
_isLoading = true;
_page = 1;
});
try {
final data = await _api.getCharacters(page: 1, status: _statusFilter, name: _searchQuery);
setState(() {
_characters = data['results'] ?? [];
_totalPages = data['info']?['pages'] ?? 1;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
}
}
调用API时传入status和name参数,支持按状态和名称筛选。
从返回的data中提取results和pages信息。
_loadMore方法加载下一页:
Future<void> _loadMore() async {
if (_isLoadingMore || _page >= _totalPages) return;
setState(() => _isLoadingMore = true);
try {
_page++;
final data = await _api.getCharacters(page: _page, status: _statusFilter, name: _searchQuery);
setState(() {
_characters.addAll(data['results'] ?? []);
_isLoadingMore = false;
});
} catch (e) {
setState(() => _isLoadingMore = false);
}
}
先检查是否已经在加载或已经到达最后一页。
页码加1后调用API获取下一页数据。
状态颜色映射
_getStatusColor方法根据角色状态返回对应的颜色:
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'alive':
return Colors.green;
case 'dead':
return Colors.red;
default:
return Colors.grey;
}
}
存活的角色用绿色,死亡的用红色,未知的用灰色。这样能直观地表达角色的状态。
页面的UI结构
页面用Column包装,包含搜索框、筛选栏和角色网格:
Widget build(BuildContext context) {
super.build(context);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
decoration: const InputDecoration(hintText: '搜索角色...', prefixIcon: Icon(Icons.search)),
onSubmitted: (value) {
_searchQuery = value.isEmpty ? null : value;
_loadCharacters();
},
),
),
搜索框用TextField实现。当用户提交搜索时,更新_searchQuery并重新加载数据。
状态筛选栏
筛选栏用水平的FilterChip列表实现:
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
_buildFilterChip('全部', null),
_buildFilterChip('存活', 'alive'),
_buildFilterChip('死亡', 'dead'),
_buildFilterChip('未知', 'unknown'),
],
),
),
用水平的ListView展示四个筛选选项。
_buildFilterChip方法创建FilterChip:
Widget _buildFilterChip(String label, String? status) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(label),
selected: _statusFilter == status,
onSelected: (selected) {
setState(() => _statusFilter = status);
_loadCharacters();
},
),
);
}
当用户点击chip时,更新_statusFilter并重新加载数据。
角色网格
角色用GridView展示,每行两列:
Expanded(
child: _isLoading
? const LoadingWidget()
: RefreshIndicator(
onRefresh: _loadCharacters,
child: GridView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: _characters.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _characters.length) {
return const Center(child: CircularProgressIndicator());
}
final character = _characters[index];
GridView用SliverGridDelegateWithFixedCrossAxisCount来定义网格布局。
crossAxisCount: 2表示每行两列。
childAspectRatio: 0.75表示卡片的宽高比为4:3。
角色卡片
每个角色用一个Card展示,包含头像和基本信息:
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => CharacterDetailScreen(character: character))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Stack(
fit: StackFit.expand,
children: [
AppNetworkImage(imageUrl: character['image'] ?? '', fit: BoxFit.cover, borderRadius: BorderRadius.zero),
Positioned(
top: 8,
right: 8,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getStatusColor(character['status'] ?? ''),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
),
卡片用Column布局,上面是头像,下面是角色信息。
头像用Stack实现,背景是角色图片,右上角有一个状态指示圆点。
圆点的颜色根据角色状态变化,用Border.all添加白色边框使其更显眼。
角色信息部分:
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(character['name'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis),
Text(character['species'] ?? '', style: TextStyle(fontSize: 10, color: Colors.grey[600]), maxLines: 1),
],
),
),
),
显示角色名称和物种。用maxLines: 1和overflow: TextOverflow.ellipsis来防止文字过长。
总结
这篇文章我们实现了一个功能完整的角色列表页面。涉及到的知识点包括:
- Tab页面管理 - 使用TabController和SingleTickerProviderStateMixin
- 页面状态保持 - 使用AutomaticKeepAliveClientMixin保持Tab页面状态
- 网格布局 - 使用GridView.builder实现高效的网格渲染
- 多条件筛选 - 支持按状态和名称筛选
- 无限滚动加载 - 通过ScrollController监听滚动事件
- 视觉设计 - 使用颜色、图标、布局来表达信息
角色列表页面展示了如何构建一个功能丰富、交互流畅的内容浏览界面。通过合理的布局、清晰的视觉设计、以及完善的交互,我们能为用户提供一个高效的浏览体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)