Add loading state tracking to widgets using context.isWaiting() for spinners and skeletons
This skill adds loading state tracking to widgets using context.isWaiting().
Shows loading indicators in widgets by:
context.isWaiting(key) to check if an operation is in progressThe Cubit method must use mix() with a key:
class UserCubit extends Cubit<User> {
void loadUser() => mix(
key: this, // Key used for tracking
() async {
final user = await api.getUser();
emit(user);
},
);
}
Check context.isWaiting(key) in the widget's build method:
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Check if loading
if (context.isWaiting(UserCubit)) {
return const Center(
child: CircularProgressIndicator(),
);
}
// Show data
final user = context.watch<UserCubit>().state;
return Text('Hello, ${user.name}');
}
}
The key in isWaiting() must match the key in mix():
// In Cubit
mix(key: this, ...) // → context.isWaiting(UserCubit)
mix(key: UserCubit, ...) // → context.isWaiting(UserCubit)
mix(key: 'loadUser', ...) // → context.isWaiting('loadUser')
mix(key: LoadAction.user, ...)// → context.isWaiting(LoadAction.user)
mix(key: (UserCubit, id), ...)// → context.isWaiting((UserCubit, id))
Widget build(BuildContext context) {
if (context.isWaiting(UserCubit)) {
return const Center(child: CircularProgressIndicator());
}
return UserProfile();
}
Widget build(BuildContext context) {
return Column(
children: [
if (context.isWaiting(UserCubit))
const LinearProgressIndicator(),
UserProfile(),
],
);
}
Widget build(BuildContext context) {
return Stack(
children: [
UserProfile(),
if (context.isWaiting(UserCubit))
Container(
color: Colors.black26,
child: const Center(child: CircularProgressIndicator()),
),
],
);
}
Widget build(BuildContext context) {
final isLoading = context.isWaiting(SaveUser);
return ElevatedButton(
onPressed: isLoading ? null : () => cubit.saveUser(user),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Save'),
);
}
Widget build(BuildContext context) {
if (context.isWaiting(ProductCubit)) {
return ListView.builder(
itemCount: 5,
itemBuilder: (_, __) => const ProductSkeleton(),
);
}
final products = context.watch<ProductCubit>().state.products;
return ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductCard(product: products[i]),
);
}
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
// Check loading for this specific item
if (context.isWaiting((DeleteItem, item.id))) {
return const ListTile(
title: Text('Deleting...'),
trailing: CircularProgressIndicator(),
);
}
return ListTile(
title: Text(item.name),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => cubit.deleteItem(item.id),
),
);
},
);
}
Widget build(BuildContext context) {
final isLoading = context.isWaiting(LoadUsers) ||
context.isWaiting(LoadPosts);
if (isLoading) {
return const CircularProgressIndicator();
}
// ...
}
Widget build(BuildContext context) {
return Column(
children: [
// Header with user loading
if (context.isWaiting(LoadUser))
const LinearProgressIndicator()
else
UserHeader(),
// Posts section with its own loading
if (context.isWaiting(LoadPosts))
const PostsSkeleton()
else
PostsList(),
],
);
}
// Cubit with multiple operations
class ProfileCubit extends Cubit<ProfileState> {
ProfileCubit() : super(const ProfileState());
void loadProfile() => mix(
key: LoadProfile,
() async {
final profile = await api.getProfile();
emit(state.copyWith(profile: profile));
},
);
void updateAvatar(File image) => mix(
key: UpdateAvatar,
() async {
final url = await api.uploadAvatar(image);
emit(state.copyWith(avatarUrl: url));
},
);
void saveSettings(Settings settings) => mix(
key: SaveSettings,
() async {
await api.saveSettings(settings);
emit(state.copyWith(settings: settings));
},
);
}
// Widget with loading states
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Full screen loading for initial load
if (context.isWaiting(LoadProfile)) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final state = context.watch<ProfileCubit>().state;
return Scaffold(
body: Column(
children: [
// Avatar with overlay loading
Stack(
children: [
CircleAvatar(backgroundImage: NetworkImage(state.avatarUrl)),
if (context.isWaiting(UpdateAvatar))
const Positioned.fill(
child: CircularProgressIndicator(),
),
],
),
// Settings section
SettingsForm(
settings: state.settings,
onSave: (s) => context.read<ProfileCubit>().saveSettings(s),
),
// Save button with loading
ElevatedButton(
onPressed: context.isWaiting(SaveSettings)
? null
: () => context.read<ProfileCubit>().saveSettings(state.settings),
child: context.isWaiting(SaveSettings)
? const CircularProgressIndicator()
: const Text('Save'),
),
],
),
);
}
}
Ask the user:
npx skills add marcglasberg/add-loading-indicator下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer