Flutter 三方库 shimmer 的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
Hello 各位小伙伴!,上海某大学计算机专业大一学生 🎓。今天来聊一个"看不见摸不着但用户体验超重要"的东西------骨架屏动画!
你有没有这种体验:打开一个 App,页面加载时是一片空白,然后内容突然蹦出来?那种"闪一下"的感觉特别不舒服!骨架屏就是来解决这个问题的!
一、什么是骨架屏?
骨架屏(Skeleton Screen)就是在内容加载时,先显示一个灰色的轮廓占位,用闪烁动画表示"正在加载中"。等真实数据到来后,骨架屏自然过渡到真实内容。
好处:
- ✅ 减少用户等待的焦虑感
- ✅ 告诉用户"数据正在路上"
- ✅ 比 Loading 动画更轻量、更现代
- ✅ 提升整体 App 品质感
二、shimmer 库介绍
shimmer 是 Flutter 里最常用的骨架屏库,它的特点:
- 纯 Dart 实现,零平台依赖
- API 简洁,一看就会
- 动画效果流畅
- 高度可定制
三、依赖配置
yaml
dependencies:
shimmer: ^3.0.0
AtomGit 适配说明:纯 Dart 库,无平台特定代码,鸿蒙适配零成本,完美运行!
四、基础用法
最简单的骨架屏
dart
import 'package:shimmer/shimmer.dart';
Shimmer.fromColors(
baseColor: Colors.grey[300]!, // 骨架基础色
highlightColor: Colors.grey[100]!, // 高亮闪烁色
child: Container(
width: 200,
height: 100,
decoration: BoxDecoration(
color: Colors.white, // 这里颜色不重要,会被覆盖
borderRadius: BorderRadius.circular(8),
),
),
)
就这么简单!三行代码就能实现闪烁效果!
五、在聊天消息列表中实战
这是我在聊天 App 里加载消息列表时用的骨架屏:
dart
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// 消息列表骨架屏
class MessageListSkeleton extends StatelessWidget {
const MessageListSkeleton({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 6, // 显示6个骨架项
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) => const _MessageSkeletonItem(),
);
}
}
/// 单条消息骨架项
class _MessageSkeletonItem extends StatelessWidget {
const _MessageSkeletonItem();
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
// 【重要】child 可以是任意 widget
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头像骨架
Container(
width: 48,
height: 48,
decoration: const BoxDecoration(
color: Colors.white, // 会被 shimmer 覆盖
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
// 文字内容骨架
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 昵称骨架
Container(
width: 100,
height: 16,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
// 消息内容骨架
Container(
width: double.infinity, // 【重点】尽量用具体宽度或自适应
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 6),
// 消息内容第二行
Container(
width: 200,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
const SizedBox(width: 8),
// 时间骨架
Container(
width: 50,
height: 12,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
);
}
}
效果预览
┌─────────────────────────────────┐
│ [○] 张三 │
│ 你好,最近怎么样? │
│ 10:30 │
├─────────────────────────────────┤
│ [○] 李四 │
│ 项目进度怎么样了? │
│ 昨天 │
├─────────────────────────────────┤
│ ...闪烁中... │
└─────────────────────────────────┘
六、聊天详情页骨架屏
dart
/// 聊天详情页骨架屏
class ChatDetailSkeleton extends StatelessWidget {
const ChatDetailSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// 对方消息
_buildMessageBubble(isMe: false),
const SizedBox(height: 12),
// 我的消息
Align(
alignment: Alignment.centerRight,
child: _buildMessageBubble(isMe: true),
),
const SizedBox(height: 12),
// 更多消息
_buildMessageBubble(isMe: false),
const SizedBox(height: 12),
_buildMessageBubble(isMe: false),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: _buildMessageBubble(isMe: true),
),
],
),
);
}
Widget _buildMessageBubble({required bool isMe}) {
return Row(
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (!isMe) ...[
Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
],
Container(
width: 200,
height: 44,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
),
),
if (isMe) const SizedBox(width: 44), // 头像占位
],
);
}
}
七、商品列表骨架屏
dart
/// 商品网格骨架屏
class ProductGridSkeleton extends StatelessWidget {
const ProductGridSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: 6,
itemBuilder: (context, index) => _ProductSkeletonItem(),
),
);
}
}
class _ProductSkeletonItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 商品图片
Container(
width: double.infinity,
height: 120,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 8),
// 商品名称
Container(
width: double.infinity,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 6),
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
// 价格
Container(
width: 60,
height: 18,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(4),
),
),
],
),
);
}
}
八、自定义 shimmer 效果
渐变方向
dart
// 从左到右渐变(默认)
Shimmer.fromColors(
direction: ShimmerDirection.ltr,
...
)
// 从右到左
Shimmer.fromColors(
direction: ShimmerDirection.rtl,
...
)
// 从上到下
Shimmer.fromColors(
direction: ShimmerDirection.ttb,
...
)
// 从下到上
Shimmer.fromColors(
direction: ShimmerDirection.btt,
...
)
自定义颜色
dart
// 浅蓝色主题
Shimmer.fromColors(
baseColor: Colors.blueGrey[100]!,
highlightColor: Colors.blueGrey[50]!,
child: ...
)
// 深色主题
Shimmer.fromColors(
baseColor: Colors.grey[800]!,
highlightColor: Colors.grey[700]!,
child: ...
)
// 品牌色主题
Shimmer.fromColors(
baseColor: const Color(0xFF6366F1).withOpacity(0.3),
highlightColor: const Color(0xFF6366F1).withOpacity(0.1),
child: ...
)
渐变区间
dart
Shimmer.fromColors(
// gradient: 自定义渐变
gradient: LinearGradient(
colors: [
Colors.grey[300]!,
Colors.grey[100]!,
Colors.grey[300]!,
],
stops: const [0.0, 0.5, 1.0], // 渐变位置
),
child: ...
)
九、配合状态管理使用
dart
class ChatListPage extends StatefulWidget {
const ChatListPage({super.key});
@override
State<ChatListPage> createState() => _ChatListPageState();
}
class _ChatListPageState extends State<ChatListPage> {
bool _isLoading = true;
List<ChatItem> _chats = [];
@override
void initState() {
super.initState();
_loadChats();
}
Future<void> _loadChats() async {
setState(() => _isLoading = true);
// 模拟网络请求
await Future.delayed(const Duration(seconds: 2));
final chats = await ChatService.getChats();
setState(() {
_chats = chats;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _isLoading
? const MessageListSkeleton() // 加载中显示骨架屏
: _buildChatList(), // 加载完成显示真实内容
);
}
Widget _buildChatList() {
return ListView.builder(
itemCount: _chats.length,
itemBuilder: (context, index) => ChatListItem(
chat: _chats[index],
onTap: () => _openChat(_chats[index]),
),
);
}
}
十、踩坑纪实
踩坑1:骨架屏颜色太深/太浅 🎨
一开始用的默认灰色,结果在深色模式下根本看不出效果。后来根据主题动态调整颜色:
dart
// 根据主题选择颜色
Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.dark
? Colors.grey[700]!
: Colors.grey[300]!,
highlightColor: Theme.of(context).brightness == Brightness.dark
? Colors.grey[600]!
: Colors.grey[100]!,
child: ...
)
踩坑2:Container 高度为 0 📏
用 Shimmer 包裹 Container 时,如果 Container 没有设置宽高,骨架屏不会显示!因为 Shimmer 的 child 是用来"裁剪"渐变的。
踩坑3:和动画冲突 🔄
我之前在一个动画组件里用了骨架屏,结果动画和 shimmer 闪烁叠在一起特别奇怪。后来把骨架屏放在 AnimatedSwitcher 里面,切换时用了渐隐效果:
dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isLoading
? const MessageListSkeleton(key: ValueKey('skeleton'))
: _buildChatList(key: const ValueKey('content')),
)
十一、效果展示

功能验证结果:
- ✅ 骨架屏加载动画正常显示
- ✅ 渐变动画流畅无卡顿
- ✅ 不同场景(列表、详情、网格)均可使用
- ✅ 深色/浅色主题适配正常
- ✅ 与数据加载状态切换正常
十二、总结心得
骨架屏真的是"小投入大回报"的功能!加上去之后 App 质感直接提升好几个档次,用户体验也好了很多。
关键收获:
- Shimmer 的 child 就是"画布",会从左到右被渐变色覆盖
- 骨架屏的每个元素宽高要尽量符合真实内容,避免布局跳动
- 要考虑深色模式的适配
- 配合 AnimatedSwitcher 使用,切换更自然
给新手的话:
别觉得这是"面子工程"!用户体验就是由这些细节决定的。一个加载友好的 App 和一个"白屏等待"的 App,给人的感觉完全不一样!
今天的分享就到这里!骨架屏虽小,但作用大大的!有问题评论区见!