Flutter for OpenHarmony 微动漫App实战:动漫新闻实现
本文介绍了Flutter实现动漫新闻页面的关键技术点。主要内容包括:1)使用FutureBuilder异步加载新闻数据,处理加载中和空数据状态;2)卡片式新闻列表设计,包含标题、图片、日期等信息;3)实现外部链接跳转功能,通过url_launcher打开原文;4)优化用户体验,包括骨架屏加载、空状态提示和错误处理。页面通过malId参数接收动漫ID,从详情页跳转时传入,展示特定动漫的相关新闻。
通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97
动漫新闻让用户了解喜欢的动漫的最新动态。微动漫App的新闻功能展示特定动漫的相关新闻,点击后跳转到原文链接。
这篇文章会实现动漫新闻页面,涉及 FutureBuilder 异步数据加载、卡片式新闻列表、图片加载处理、外部链接跳转等技术点。

新闻页面的设计思路
新闻页面是从动漫详情页进入的,展示该动漫的相关新闻。每条新闻包含标题、描述、图片、日期,点击后打开原文。
列表形式:新闻数量不定,用 ListView 展示最合适。
卡片设计:每条新闻一张卡片,图片在上,文字在下,层次分明。
外部跳转:新闻原文在外部网站,点击后用浏览器打开。
页面参数传递
新闻页面需要知道是哪个动漫的新闻:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../services/api_service.dart';
import '../models/character.dart';
import '../widgets/shimmer_loading.dart';
class AnimeNewsScreen extends StatefulWidget {
final int malId;
const AnimeNewsScreen({super.key, required this.malId});
State<AnimeNewsScreen> createState() => _AnimeNewsScreenState();
}
malId 是动漫的唯一标识,通过构造函数传入。required 关键字表示这个参数必须提供。
从详情页跳转时传入:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AnimeNewsScreen(malId: anime.malId),
),
);
FutureBuilder 加载数据
class _AnimeNewsScreenState extends State<AnimeNewsScreen> {
late Future<List<AnimeNews>> _newsFuture;
void initState() {
super.initState();
_newsFuture = ApiService.getAnimeNews(widget.malId);
}
late 关键字表示变量会在使用前初始化。在 initState 里调用 API 获取新闻数据。
widget.malId 访问 StatefulWidget 的属性,这是 State 类访问 Widget 参数的方式。
FutureBuilder 的使用
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('新闻')),
body: FutureBuilder<List<AnimeNews>>(
future: _newsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const ShimmerLoading(itemCount: 8, isGrid: false);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.newspaper, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无新闻',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
],
),
);
}
final news = snapshot.data!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: news.length,
itemBuilder: (_, i) => _buildNewsCard(news[i]),
);
},
),
);
}
FutureBuilder 根据 Future 的状态自动重建 UI。snapshot.connectionState 表示当前状态:waiting 是加载中,done 是完成。
snapshot.hasData 检查是否有数据,snapshot.data 获取数据。注意要处理数据为空的情况。
加载状态处理
if (snapshot.connectionState == ConnectionState.waiting) {
return const ShimmerLoading(itemCount: 8, isGrid: false);
}
加载中显示骨架屏,8 个占位项,列表形式。骨架屏比转圈圈更友好,用户能预期内容的布局。
空状态处理
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.newspaper, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无新闻',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
],
),
);
}
没有新闻时显示空状态。Icons.newspaper 是报纸图标,和新闻主题呼应。
空状态要居中显示,图标 + 文字的组合比单独的文字更友好。
新闻列表
final news = snapshot.data!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: news.length,
itemBuilder: (_, i) => _buildNewsCard(news[i]),
);
ListView.builder 懒加载列表,只构建可见的项。padding 设置列表的内边距。
itemBuilder 的第一个参数是 context,这里用 _ 表示不使用。
新闻卡片结构
Widget _buildNewsCard(AnimeNews news) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: InkWell(
onTap: () async {
if (news.link != null) {
try {
await launchUrl(Uri.parse(news.link!));
} catch (e) {
print('❌ Error launching URL: $e');
}
}
},
Card 提供卡片样式,自带阴影和圆角。margin 设置卡片之间的间距。
InkWell 让卡片可点击,点击时有水波纹效果。onTap 里调用 launchUrl 打开外部链接。
外部链接跳转
onTap: () async {
if (news.link != null) {
try {
await launchUrl(Uri.parse(news.link!));
} catch (e) {
print('❌ Error launching URL: $e');
}
}
},
先检查链接是否存在,然后用 launchUrl 打开。Uri.parse 把字符串转成 Uri 对象。
try-catch 捕获可能的异常,比如链接格式错误或设备无法打开链接。
卡片内容布局
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (news.imageUrl?.isNotEmpty ?? false)
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
child: _buildImage(news.imageUrl!),
),
Column 垂直排列图片和文字。crossAxisAlignment.start 让内容左对齐。
图片用 ClipRRect 裁剪,只有顶部两个角是圆角,和 Card 的圆角对齐。
news.imageUrl?.isNotEmpty ?? false 是空安全的写法:如果 imageUrl 为 null 或空字符串,就不显示图片。
图片加载组件
Widget _buildImage(String imageUrl) {
return SizedBox(
height: 150,
width: double.infinity,
child: Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(color: Colors.grey[300]);
},
errorBuilder: (context, error, stackTrace) {
return Container(color: Colors.grey[300]);
},
),
);
}
SizedBox 固定图片高度为 150,宽度撑满。BoxFit.cover 让图片填满容器,可能会裁剪。
loadingBuilder 处理加载中状态,显示灰色占位。loadingProgress == null 表示加载完成。
errorBuilder 处理加载失败,也显示灰色占位,不会显示破图标。
新闻标题
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
news.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
标题用粗体,最多显示 2 行。TextOverflow.ellipsis 超出部分显示省略号。
Padding 给文字区域加内边距,和图片区分开。
新闻描述
if (news.description?.isNotEmpty ?? false) ...[
const SizedBox(height: 8),
Text(
news.description!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
描述是可选的,用 if 判断是否显示。…[] 是展开操作符,把列表里的元素展开到外层列表。
描述用灰色小字,和标题形成层次。同样限制 2 行,保持卡片高度一致。
新闻日期
if (news.date?.isNotEmpty ?? false) ...[
const SizedBox(height: 8),
Text(
news.date!,
style: TextStyle(color: Colors.grey[500], fontSize: 11),
),
],
],
),
),
],
),
),
);
}
日期也是可选的,用更浅的灰色和更小的字号,作为辅助信息。
下拉刷新
可以加上下拉刷新功能:
body: FutureBuilder<List<AnimeNews>>(
future: _newsFuture,
builder: (context, snapshot) {
// 加载和空状态处理
final news = snapshot.data!;
return RefreshIndicator(
onRefresh: () async {
setState(() {
_newsFuture = ApiService.getAnimeNews(widget.malId);
});
},
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: news.length,
itemBuilder: (_, i) => _buildNewsCard(news[i]),
),
);
},
),
RefreshIndicator 包裹 ListView,下拉时显示刷新指示器。onRefresh 里重新请求数据。
注意要用 setState 更新 _newsFuture,这样 FutureBuilder 才会重新构建。
错误处理增强
FutureBuilder 还可以处理错误状态:
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const ShimmerLoading(itemCount: 8, isGrid: false);
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red[300]),
const SizedBox(height: 16),
Text(
'加载失败',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
_newsFuture = ApiService.getAnimeNews(widget.malId);
});
},
child: const Text('重试'),
),
],
),
);
}
// 正常数据处理
}
snapshot.hasError 检查是否有错误。错误状态显示错误图标和重试按钮。
新闻卡片动画
给卡片加入场动画:
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: news.length,
itemBuilder: (_, i) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: Duration(milliseconds: 300 + i * 50),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: _buildNewsCard(news[i]),
);
},
);
TweenAnimationBuilder 创建补间动画。每个卡片的动画延迟不同(i * 50),形成依次入场的效果。
Opacity 控制透明度,Transform.translate 控制位移,组合起来就是从下往上淡入的效果。
分享新闻
长按卡片可以分享:
InkWell(
onTap: () async {
// 打开链接
},
onLongPress: () {
showModalBottomSheet(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.share),
title: const Text('分享'),
onTap: () {
Navigator.pop(context);
// 调用分享功能
},
),
ListTile(
leading: const Icon(Icons.copy),
title: const Text('复制链接'),
onTap: () {
Navigator.pop(context);
Clipboard.setData(ClipboardData(text: news.link ?? ''));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('链接已复制')),
);
},
),
],
),
);
},
// 卡片内容
)
onLongPress 处理长按事件,弹出底部菜单。showModalBottomSheet 显示底部弹窗。
菜单里提供分享和复制链接两个选项,满足不同需求。
深色模式适配
卡片在深色模式下自动适配,但图片占位色可以调整:
Widget _buildImage(String imageUrl) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final placeholderColor = isDark ? Colors.grey[800] : Colors.grey[300];
return SizedBox(
height: 150,
width: double.infinity,
child: Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(color: placeholderColor);
},
errorBuilder: (context, error, stackTrace) {
return Container(color: placeholderColor);
},
),
);
}
深色模式下用深灰色占位,浅色模式下用浅灰色,和背景协调。
小结
动漫新闻页面涉及的技术点:FutureBuilder 异步加载、ListView.builder 列表、Card 卡片组件、InkWell 点击效果、url_launcher 外部链接、Image.network 网络图片、ClipRRect 圆角裁剪。
页面结构清晰:参数传递 → 数据加载 → 状态处理 → 列表渲染 → 卡片构建。
新闻卡片的设计要点:图片在上吸引眼球,标题粗体突出重点,描述和日期作为补充信息。点击跳转外部链接,长按可以分享或复制。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)