Flutter for OpenHarmony 万能游戏库App实战 - 首页宝可梦图鉴推荐实现
Flutter宝可梦图鉴实现要点 本文介绍了如何在Flutter应用中实现宝可梦图鉴功能,主要包含以下关键点: API设计优化 - 使用PokeAPI的分页接口获取基础数据,通过ID拼接图片URL避免额外请求 图片处理技巧 - 利用GitHub官方图库直接获取高清立绘,格式为/pokemon/other/official-artwork/{id}.png 性能优化 - 使用Future.wait并
Flutter for OpenHarmony 万能游戏库App实战 - 首页宝可梦图鉴推荐实现
宝可梦作为全球最知名的IP之一,在游戏库App中加入宝可梦图鉴功能是个不错的选择。PokeAPI是一个完全免费、无需注册的API,提供了非常丰富的宝可梦数据。这篇文章我们来实现首页的宝可梦推荐列表,和上一篇的游戏推荐不同,这次我们会更深入地探讨图片URL拼接、并行请求、以及卡片样式的差异化设计。
从API说起
PokeAPI的设计很有意思,它不会一次性返回所有数据,而是采用分页+详情的模式。先看看获取列表的接口:
class PokemonApi {
static const String baseUrl = 'https://pokeapi.co/api/v2';
final ApiService _api = ApiService();
Future<Map<String, dynamic>> getPokemonList({int offset = 0, int limit = 20}) async {
final result = await _api.get('$baseUrl/pokemon?offset=$offset&limit=$limit');
return result as Map<String, dynamic>;
}
这个接口返回的是宝可梦的名称和详情URL列表,并不包含图片等详细信息。offset和limit参数用于分页,默认获取前20个。为什么这样设计?因为宝可梦有上千只,一次性返回所有详情数据量太大了。
返回的数据结构大概是这样的:
{
"results": [
{"name": "bulbasaur", "url": "https://pokeapi.co/api/v2/pokemon/1/"},
{"name": "ivysaur", "url": "https://pokeapi.co/api/v2/pokemon/2/"}
]
}
注意这里只有name和url,没有图片地址。但我们可以根据宝可梦的ID来拼接图片URL,这是个小技巧,后面会讲到。
图片URL的拼接技巧
PokeAPI官方提供了一个GitHub仓库专门存放宝可梦图片,URL格式是固定的:
https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/{id}.png
这个地址指向的是官方高清立绘,比API返回的小图好看多了。只要知道宝可梦的ID,就能直接拼出图片地址,不需要额外请求详情接口。
在代码中是这样用的:
AppNetworkImage(
imageUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png',
width: 60,
height: 60,
),
这里的
$id就是宝可梦的编号。由于列表接口返回的顺序是按编号排列的,所以index + 1就是对应的ID。这样做的好处是省去了N次详情请求,首页加载速度快了很多。
并行加载多个数据源
首页不只有宝可梦,还有游戏推荐等其他内容。如果一个一个串行加载,用户要等很久。我们用Future.wait来并行加载:
Future<void> _loadData() async {
setState(() {
_isLoading = true;
_gamesError = null;
_pokemonError = null;
});
await Future.wait([
_loadGames(),
_loadPokemon(),
]);
if (mounted) {
setState(() => _isLoading = false);
}
}
Future.wait接收一个Future列表,同时发起所有请求,等全部完成后才继续执行。这样两个接口的耗时是取最长的那个,而不是相加。比如游戏接口要2秒,宝可梦接口要1秒,总耗时是2秒而不是3秒。
加载宝可梦数据的具体实现:
Future<void> _loadPokemon() async {
try {
final pokemon = await _pokemonApi.getPokemonList(limit: 10);
if (mounted) {
setState(() {
_featuredPokemon = pokemon['results'] ?? [];
_pokemonError = null;
});
}
} catch (e) {
if (mounted) {
setState(() {
_pokemonError = e.toString();
_featuredPokemon = [];
});
}
}
}
首页只展示10个宝可梦,所以
limit: 10。注意这里用了??空合并运算符,如果results为null就返回空列表,避免后续代码报空指针错误。
宝可梦卡片的UI设计
宝可梦卡片和游戏卡片的设计思路不太一样。游戏卡片是横向的大图+文字,宝可梦卡片是纵向的小卡片,更紧凑:
return SizedBox(
height: 140,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: _featuredPokemon.length,
整个列表高度固定140,比游戏列表的200要矮。因为宝可梦卡片内容简单,不需要那么高。padding设置左右12的内边距,和游戏列表保持一致,视觉上更协调。
每个卡片的构建:
itemBuilder: (context, index) {
final pokemon = _featuredPokemon[index];
final id = index + 1;
return Container(
width: 100,
margin: const EdgeInsets.symmetric(horizontal: 4),
child: Card(
child: InkWell(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => PokemonDetailScreen(pokemonId: id))),
borderRadius: BorderRadius.circular(16),
卡片宽度100,比游戏卡片的260窄很多。这样一屏能显示更多宝可梦,符合图鉴浏览的使用场景。用户想快速浏览有哪些宝可梦,而不是仔细看每一个的详情。
卡片内部的布局:
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AppNetworkImage(
imageUrl: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/$id.png',
width: 60,
height: 60,
),
const SizedBox(height: 8),
Text(
pokemon['name'].toString().toUpperCase(),
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text('#${id.toString().padLeft(3, '0')}', style: TextStyle(fontSize: 10, color: Colors.grey[600])),
],
),
布局很简单:图片、名称、编号,垂直居中排列。名称用
toUpperCase()转成大写,这是宝可梦的传统展示方式。编号用padLeft(3, '0')补零,比如1变成001,25变成025,看起来更专业。
处理加载失败的情况
网络请求难免会失败,我们要给用户一个友好的提示和重试的机会:
if (_pokemonError != null) {
return SizedBox(
height: 140,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.cloud_off, size: 40, color: Colors.grey[400]),
const SizedBox(height: 8),
Text(l10n.loadingFailed, style: TextStyle(color: Colors.grey[600], fontSize: 12)),
错误状态的容器高度也是140,和正常状态保持一致。这样切换状态时页面不会跳动。图标用cloud_off,一眼就能看出是网络问题。
重试按钮:
TextButton.icon(
onPressed: _loadPokemon,
icon: const Icon(Icons.refresh, size: 16),
label: Text(l10n.retry, style: const TextStyle(fontSize: 12)),
),
],
),
),
);
}
点击重试只会重新加载宝可梦数据,不会影响其他已经加载成功的内容。这比整个页面刷新体验好多了。按钮和文字都用了较小的字号,因为这个区域本身就不大。
空数据的处理
有时候接口返回成功了,但数据是空的,这种情况也要处理:
if (_featuredPokemon.isEmpty) {
return SizedBox(
height: 140,
child: Center(child: Text(l10n.noData, style: const TextStyle(color: Colors.grey))),
);
}
这种情况比较少见,但防御性编程是个好习惯。万一API改了返回格式,或者服务器出了问题返回空数据,用户看到的是"暂无数据"而不是一片空白或者报错。
点击跳转详情页
点击宝可梦卡片会跳转到详情页,我们把ID传过去:
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => PokemonDetailScreen(pokemonId: id))),
这里传的是数字ID而不是名称,因为后续请求详情接口用ID更方便。详情页会根据这个ID去获取完整的宝可梦信息,包括属性、技能、种族值等。
关于国际化
你可能注意到代码里用了l10n.loadingFailed、l10n.retry这样的写法,这是Flutter的国际化方案:
Widget _buildPokemonList(AppLocalizations l10n) {
方法参数里传入了
AppLocalizations对象,这样方法内部就能使用多语言文本了。把国际化对象作为参数传递,比在方法内部重新获取更高效,因为build方法可能会频繁调用。
和游戏推荐的对比
写到这里,我们可以对比一下宝可梦推荐和游戏推荐的异同:
相同点:
- 都是横向滚动列表
- 都有加载中、加载失败、空数据三种状态
- 都支持点击跳转详情
不同点:
- 卡片尺寸不同(100 vs 260宽度)
- 图片来源不同(拼接URL vs API返回)
- 展示信息不同(名称+编号 vs 名称+类型)
这种差异化设计是有意为之的。不同类型的内容用不同的展示方式,用户一眼就能区分,也不会产生视觉疲劳。
性能优化的思考
虽然这个功能看起来简单,但有几个性能相关的点值得注意:
1. 图片懒加载
ListView.builder本身就是懒加载的,只有即将显示的item才会被构建。配合网络图片组件的loading状态,用户体验很流畅。
2. 避免重复请求
我们在State里缓存了_featuredPokemon,页面rebuild时不会重新请求。只有下拉刷新或者点击重试才会重新加载。
3. 合理的数据量
首页只加载10个宝可梦,数据量很小。如果用户想看更多,可以点击"查看全部"进入完整的图鉴列表页。
写在最后
这篇文章我们实现了首页的宝可梦图鉴推荐功能。虽然代码量不大,但涉及到的知识点还挺多的:API设计理解、图片URL拼接、并行请求、差异化UI设计、错误处理等等。
做App开发就是这样,看起来简单的功能,要做好其实有很多细节要考虑。希望这篇文章对你有所帮助,有问题欢迎留言讨论。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)