hollow 0.1.1
hollow: ^0.1.1 copied to clipboard
Pixel-perfect skeleton loading screens for Flutter. Wrap any widget in Skeleton — hollow snapshots the real render tree and shows an animated shimmer placeholder.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:hollow/hollow.dart';
import 'bones/bones_registry.dart';
void main() {
registerAllBones();
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const FeedScreen(),
);
}
}
class FeedScreen extends StatefulWidget {
const FeedScreen({super.key});
@override
State<FeedScreen> createState() => _FeedScreenState();
}
class _FeedScreenState extends State<FeedScreen> {
bool _loading = true;
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) setState(() => _loading = false);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('hollow demo')),
body: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: 5,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (_, i) => Skeleton(
name: 'post-card',
loading: _loading,
fixture: const PostCard(
title: 'How to build pixel-perfect skeletons in Flutter',
subtitle: 'A deep dive into the RenderObject tree',
author: 'r4yb3l',
readTime: '4 min read',
),
child: PostCard(
title: 'Article number ${i + 1}',
subtitle: 'Real content loaded from the API after the delay',
author: 'r4yb3l',
readTime: '${i + 2} min read',
),
),
),
);
}
}
class PostCard extends StatelessWidget {
const PostCard({
super.key,
required this.title,
required this.subtitle,
required this.author,
required this.readTime,
});
final String title;
final String subtitle;
final String author;
final String readTime;
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 160,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 12),
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Row(
children: [
const CircleAvatar(radius: 14),
const SizedBox(width: 8),
Text(author, style: Theme.of(context).textTheme.labelMedium),
const Spacer(),
Text(readTime, style: Theme.of(context).textTheme.labelSmall),
],
),
],
),
),
);
}
}