开源鸿蒙 Flutter 实战|用户详情页骨架屏加载效果完整实现
【摘要】本文基于Flutter框架实现开源鸿蒙用户详情页骨架屏加载效果,包含组件封装、状态管理等核心功能。使用OpenHarmony官方兼容库开发,通过Shimmer组件实现动态加载动画,支持深色/浅色模式自动适配。文中详细展示了用户详情页专属骨架屏组件的实现代码,包括头部区域、信息卡片和个人简介的骨架布局,并解决了用户卡片点击无响应的问题。该方案已在鸿蒙虚拟机验证通过,提供平滑过渡效果和清晰的代
🎨 开源鸿蒙 Flutter 实战|用户详情页骨架屏加载效果完整实现
欢迎加入开源鸿蒙跨平台社区→https://openharmonycrosplatform.csdn.net
【摘要】本文面向开源鸿蒙跨平台开发新手,基于 Flutter 框架实现用户详情页的骨架屏加载效果,包含骨架屏组件封装、页面状态管理、点击事件修复、深色模式适配等核心内容,完整覆盖功能实现、问题排查、鸿蒙适配要点与虚拟机实机运行验证,代码可直接复制复用,有效提升应用用户
之前给首页列表加了骨架屏,这次直接把用户详情页也安排上了!不仅实现了完整的详情页骨架屏布局,还踩坑修复了用户卡片点击无响应的问题,全程用的都是 OpenHarmony 官方兼容库,已经在鸿蒙虚拟机完整验证,点击用户卡片就能跳转,骨架屏加载超丝滑!
先给大家汇报一下这次的核心成果✨:
✅ 封装用户详情页专属骨架屏组件
✅ 完整模拟真实页面布局结构
✅ 修复用户卡片点击无响应的问题
✅ 深色 / 浅色模式自动适配
✅ 加载完成后平滑过渡到实际内容
✅ 鸿蒙虚拟机实机验证,交互完全正常
✅ 代码结构清晰,新手也能看懂、能修改
一、技术选型说明
全程使用 OpenHarmony 官方兼容清单内的稳定库,无额外依赖,完全规避兼容风险:
二、核心功能实现
2.1 第一步:扩展骨架屏组件库
在之前的lib/widgets/shimmer_skeleton.dart基础上,新增用户详情页专属骨架屏组件:
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// 基础骨架屏组件
class ShimmerSkeleton extends StatelessWidget {
final double width;
final double height;
final double radius;
final bool isDarkMode;
const ShimmerSkeleton({
super.key,
required this.width,
required this.height,
this.radius = 8,
required this.isDarkMode,
});
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!,
highlightColor: isDarkMode ? Colors.grey[700]! : Colors.grey[100]!,
period: const Duration(milliseconds: 1500),
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(radius),
),
),
);
}
}
/// 用户详情页骨架屏
class UserDetailSkeleton extends StatelessWidget {
final bool isDarkMode;
const UserDetailSkeleton({super.key, required this.isDarkMode});
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 头部区域
Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isDarkMode
? [Colors.grey[800]!, Colors.grey[900]!]
: [Colors.purple[100]!, Colors.purple[50]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 头像骨架
ShimmerSkeleton(
width: 100,
height: 100,
radius: 50,
isDarkMode: isDarkMode,
),
const SizedBox(height: 16),
// 用户名骨架
ShimmerSkeleton(
width: 150,
height: 24,
radius: 12,
isDarkMode: isDarkMode,
),
const SizedBox(height: 8),
// 简介骨架
ShimmerSkeleton(
width: 200,
height: 16,
radius: 8,
isDarkMode: isDarkMode,
),
],
),
),
const SizedBox(height: 24),
// 信息卡片区域
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: List.generate(4, (index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShimmerSkeleton(
width: 40,
height: 40,
radius: 20,
isDarkMode: isDarkMode,
),
const SizedBox(height: 12),
ShimmerSkeleton(
width: 60,
height: 16,
radius: 8,
isDarkMode: isDarkMode,
),
const SizedBox(height: 4),
ShimmerSkeleton(
width: 40,
height: 20,
radius: 10,
isDarkMode: isDarkMode,
),
],
),
),
);
}),
),
),
const SizedBox(height: 24),
// 个人简介区域
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerSkeleton(
width: 80,
height: 20,
radius: 10,
isDarkMode: isDarkMode,
),
const SizedBox(height: 12),
...List.generate(3, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ShimmerSkeleton(
width: double.infinity,
height: 14,
radius: 7,
isDarkMode: isDarkMode,
),
);
}),
],
),
),
),
),
const SizedBox(height: 24),
],
),
);
}
}
2.2 第二步:修改用户详情页
将用户详情页从StatelessWidget改为StatefulWidget,添加加载状态控制:
// 导入骨架屏组件
import 'widgets/shimmer_skeleton.dart';
// 用户详情页
class UserDetailPage extends StatefulWidget {
final GitHubUser user;
const UserDetailPage({super.key, required this.user});
State<UserDetailPage> createState() => _UserDetailPageState();
}
class _UserDetailPageState extends State<UserDetailPage> {
bool _isLoading = true;
void initState() {
super.initState();
// 模拟网络请求延迟
Future.delayed(const Duration(milliseconds: 800), () {
if (mounted) {
setState(() {
_isLoading = false;
});
}
});
}
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// 加载中:显示骨架屏
if (_isLoading) {
return Scaffold(
appBar: AppBar(title: const Text("用户详情")),
body: UserDetailSkeleton(isDarkMode: isDarkMode),
);
}
// 加载完成:显示实际内容
return Scaffold(
appBar: AppBar(title: Text(widget.user.login)),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 头部区域
Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isDarkMode
? [Colors.grey[800]!, Colors.grey[900]!]
: [Colors.purple[100]!, Colors.purple[50]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: 'avatar-${widget.user.id}',
child: CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(widget.user.avatarUrl),
),
),
const SizedBox(height: 16),
Text(
widget.user.login,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
widget.user.htmlUrl,
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
),
const SizedBox(height: 24),
// 信息卡片区域
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
_buildInfoCard(Icons.people, "关注者", "1.2k", isDarkMode),
_buildInfoCard(Icons.person_add, "正在关注", "256", isDarkMode),
_buildInfoCard(Icons.folder, "仓库", "42", isDarkMode),
_buildInfoCard(Icons.location_on, "位置", "中国", isDarkMode),
],
),
),
const SizedBox(height: 24),
// 个人简介区域
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"个人简介",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
"这是一位热爱开源的开发者,专注于Flutter和开源鸿蒙跨平台开发,喜欢分享技术心得,欢迎一起交流学习~",
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
height: 1.6,
),
),
],
),
),
),
),
const SizedBox(height: 24),
],
),
),
);
}
Widget _buildInfoCard(IconData icon, String label, String value, bool isDarkMode) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 32, color: Theme.of(context).primaryColor),
const SizedBox(height: 12),
Text(
label,
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
}
2.3 第三步:修复用户卡片点击无响应问题
问题原因:
之前的代码中,_UserCardContent组件里的AnimatedCard的onTap回调是空的() {},这会拦截点击事件,导致外层的OpenContainer无法接收到点击事件,从而无法打开用户详情页。
修复方案:
1.给_UserCardContent添加onTap参数
2.将OpenContainer的openContainer回调传递给_UserCardContent
3.在AnimatedCard的onTap中正确调用传入的回调
// 修改前
closedBuilder: (context, openContainer) => _UserCardContent(user: user),
class _UserCardContent extends StatelessWidget {
final GitHubUser user;
const _UserCardContent({required this.user});
Widget build(BuildContext context) {
return AnimatedCard(
onTap: () {}, // 空回调拦截了点击事件
// ...
);
}
}
// 修改后
closedBuilder: (context, openContainer) => _UserCardContent(
user: user,
onTap: openContainer, // 传递 openContainer 回调
),
class _UserCardContent extends StatelessWidget {
final GitHubUser user;
final VoidCallback onTap; // 新增参数
const _UserCardContent({required this.user, required this.onTap});
Widget build(BuildContext context) {
return AnimatedCard(
onTap: onTap, // 正确调用 openContainer
// ...
);
}
}
三、开源鸿蒙平台适配要点
3.1 动画性能优化
使用shimmer库的官方稳定版 3.0.0,在鸿蒙设备上渲染性能最好
骨架屏动画时长控制在 1500ms,符合开源鸿蒙系统视觉规范
加载完成后直接切换到实际内容,避免不必要的动画叠加
3.2 深色模式适配
通过Theme.of(context).brightness判断当前主题模式,自动调整骨架屏的baseColor和highlightColor,确保在深色模式下有足够的对比度,无视觉异常。
3.3 状态管理注意事项
用户详情页改为StatefulWidget后,要在initState中模拟加载延迟
使用mounted判断,避免页面销毁后调用setState导致的异常
加载延迟控制在 800ms 左右,既能展示骨架屏效果,又不会让用户等待太久
3.4 权限说明
所有功能均为纯 UI 实现,无需申请任何开源鸿蒙系统权限,直接接入即可使用。
四、开源鸿蒙虚拟机运行验证
一键运行命令
cd ohos
hvigorw assembleHap -p product=default -p buildMode=debug
hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b com.example.demo1
Flutter 开源鸿蒙用户详情页骨架屏 - 虚拟机全屏运行验证

效果:应用在开源鸿蒙虚拟机全屏稳定运行,所有功能正常,无闪退、无渲染异常
五、新手学习总结
作为大一新生,这次用户详情页骨架屏的实现真的让我收获满满!不仅学会了如何封装复杂页面的骨架屏,还踩坑修复了点击事件拦截的问题,而且全程都能完美兼容开源鸿蒙平台,成就感直接拉满🥰
这次开发也让我明白了:骨架屏的布局要尽量和真实页面一致,这样用户体验才会连贯。手势事件的嵌套一定要注意,内层的空回调会拦截外层的点击事件,这个坑在鸿蒙平台上会更明显。页面状态管理要注意mounted判断,避免页面销毁后调用setState导致的异常
后续我还会继续优化:
✅ 实现更多页面的骨架屏
✅ 优化骨架屏的动画效果
✅ 对接真实后端接口实现动态加载
✅ 增加更多用户详情页的功能
也会持续给大家分享我的鸿蒙 Flutter 新手实战内容,和大家一起在开源鸿蒙的生态里慢慢进步✨
如果这篇文章有帮到你,或者你也有更好的骨架屏实现思路,欢迎在评论区和我交流呀!
更多推荐




所有评论(0)