skeletonizer_plus 1.2.0
skeletonizer_plus: ^1.2.0 copied to clipboard
Skeleton loading states for Flutter with shimmer animations, theme-aware colours, automatic tree analysis, and Sliver support.
import 'package:flutter/material.dart';
import 'package:skeletonizer_plus/skeletonizer_plus.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Skeletonizer Plus Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: ThemeMode.system,
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _isLoading = true;
int _selectedIndex = 0;
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) setState(() => _isLoading = false);
});
}
void _toggleLoading() => setState(() => _isLoading = !_isLoading);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Skeletonizer Plus Example'),
actions: [
IconButton(
icon: Icon(_isLoading ? Icons.pause : Icons.play_arrow),
onPressed: _toggleLoading,
tooltip: 'Toggle Loading',
),
],
),
body: IndexedStack(
index: _selectedIndex,
children: [
AutomaticSkeletonExample(isLoading: _isLoading),
CustomBonesExample(isLoading: _isLoading),
ListViewExample(isLoading: _isLoading),
SliverExample(isLoading: _isLoading),
NewFeaturesExample(isLoading: _isLoading),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) =>
setState(() => _selectedIndex = index),
destinations: const [
NavigationDestination(icon: Icon(Icons.auto_awesome), label: 'Automatic'),
NavigationDestination(icon: Icon(Icons.build), label: 'Custom'),
NavigationDestination(icon: Icon(Icons.list), label: 'List'),
NavigationDestination(icon: Icon(Icons.view_carousel), label: 'Sliver'),
NavigationDestination(icon: Icon(Icons.new_releases), label: 'New'),
],
),
);
}
}
class AutomaticSkeletonExample extends StatelessWidget {
const AutomaticSkeletonExample({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Automatic Skeleton Generation',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SkeletonizerPlus(
enabled: isLoading,
child: const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(radius: 24, child: Icon(Icons.person)),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'John Doe',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text('Software Engineer'),
],
),
),
],
),
SizedBox(height: 16),
Text(
'This is a sample card with multiple text elements and an '
'avatar. The skeletonizer detects each element and '
'generates matching bones.',
),
],
),
),
),
),
],
),
);
}
}
class CustomBonesExample extends StatelessWidget {
const CustomBonesExample({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Custom Bone Layout',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
SkeletonizerPlus.custom(
enabled: isLoading,
bones: const [
BoneCircle(radius: 30),
BoneText(words: 4),
BoneText(words: 6, lines: 2),
BoneRect(width: 300, height: 200, radius: 12),
BoneIcon(size: 32),
],
baseColor: Colors.blue.shade200,
highlightColor: Colors.blue.shade50,
speed: const Duration(milliseconds: 800),
),
],
),
);
}
}
class ListViewExample extends StatelessWidget {
const ListViewExample({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return SkeletonizerPlus(
enabled: isLoading,
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text('Item ${index + 1}'),
subtitle: Text('This is item number ${index + 1}'),
trailing: const Icon(Icons.arrow_forward_ios),
),
),
),
);
}
}
class SliverExample extends StatelessWidget {
const SliverExample({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
const SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(title: Text('Sliver Example')),
),
SliverSkeletonizerPlus(
enabled: isLoading,
child: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Card(
margin:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text('Item ${index + 1}'),
subtitle: Text('This is item number ${index + 1}'),
),
),
childCount: 20,
),
),
),
],
);
}
}
class NewFeaturesExample extends StatelessWidget {
const NewFeaturesExample({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'SkeletonIgnore — keep widgets visible',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SkeletonizerPlus(
enabled: isLoading,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.person, size: 40),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Skeletonized Text'),
const SizedBox(height: 8),
SkeletonIgnore(
child: ElevatedButton(
onPressed: () {},
child: const Text('I am Ignored!'),
),
),
],
),
),
],
),
),
),
),
const SizedBox(height: 24),
const Text(
'SkeletonUnite — collapse a subtree',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SkeletonizerPlus(
enabled: isLoading,
child: const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Three icons united into one bone:'),
SizedBox(height: 8),
SkeletonUnite(
child: Row(
children: [
Icon(Icons.star),
SizedBox(width: 8),
Icon(Icons.star),
SizedBox(width: 8),
Icon(Icons.star),
],
),
),
],
),
),
),
),
const SizedBox(height: 24),
const Text(
'Pulse Animation',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SkeletonizerPlus.withConfig(
enabled: isLoading,
animationConfig: const AnimationConfig(type: AnimationType.pulse),
child: const Card(
child: ListTile(
leading: Icon(Icons.favorite),
title: Text('Pulsing Skeleton'),
subtitle: Text('Opacity fade instead of shimmer.'),
),
),
),
],
),
);
}
}