بنقرة واحدة
use-composite-key
Use Dart records as composite keys for per-item or per-parameter loading and error tracking
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Use Dart records as composite keys for per-item or per-parameter loading and error tracking
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Add custom error handling with catchError to a mix() call for logging, error conversion, or suppression
Add internet connectivity checking before executing a Cubit method with optional retry
Add debounce to delay method execution until after a period of inactivity for search or validation
Add EffectQueue to state for triggering multiple sequential one-time UI effects
Add Effect fields to state class for one-time UI notifications like snackbars, navigation, or form clearing
Add error state handling to widgets using context.isFailed() and context.getException()
| name | use-composite-key |
| description | Use Dart records as composite keys for per-item or per-parameter loading and error tracking |
This skill uses Dart records as composite keys for per-item or per-parameter loading/error tracking.
Uses record keys like (CubitType, id) to:
Use composite keys when:
Instead of key: this, use a record combining the action and parameter:
class ProductCubit extends Cubit<ProductState> {
// Per-product loading state
void loadProduct(String productId) => mix(
key: (LoadProduct, productId), // Composite key
() async {
final product = await api.getProduct(productId);
emit(state.copyWith(
products: {...state.products, productId: product},
));
},
);
// Per-product delete state
void deleteProduct(String productId) => mix(
key: (DeleteProduct, productId), // Different action, same pattern
() async {
await api.deleteProduct(productId);
emit(state.copyWith(
products: state.products..remove(productId),
));
},
);
}
Use the exact same key structure in the widget:
Widget build(BuildContext context) {
return ListView.builder(
itemCount: productIds.length,
itemBuilder: (context, index) {
final productId = productIds[index];
// Check loading for THIS specific product
if (context.isWaiting((LoadProduct, productId))) {
return const ProductSkeleton();
}
// Check error for THIS specific product
if (context.isFailed((LoadProduct, productId))) {
return ProductErrorTile(
error: context.getException((LoadProduct, productId)),
onRetry: () => cubit.loadProduct(productId),
);
}
return ProductTile(product: state.products[productId]!);
},
);
}
| Key Type | Example | Use Case |
|---|---|---|
this / Type | key: this → UserCubit | Single operation per Cubit |
| String | key: 'loadData' | Named operations |
| Enum | key: Action.load | Categorized operations |
| Record | key: (Action, id) | Per-parameter operations |
thisWhen using key: this inside a Cubit method, the key becomes the Cubit's runtimeType, not
the instance. This means all instances of the same Cubit type share the same key for state
tracking.
// Both instances share the same key (UserCubit)
final cubit1 = UserCubit();
final cubit2 = UserCubit();
cubit1.loadUser(); // key: UserCubit
cubit2.loadUser(); // key: UserCubit (same key!)
Dart's standard equality applies to keys:
| Type | Equality | Example |
|---|---|---|
| Primitives | Value-based | 'abc' == 'abc' ✓ |
| Records | Structural | (LoadUser, 'abc') == (LoadUser, 'abc') ✓ |
| Objects | Identity (unless == overridden) | MyClass() != MyClass() |
| Types | Identity | UserCubit == UserCubit ✓ |
// In Cubit
mix(key: (UserCubit, userId), ...)
// In Widget
context.isWaiting((UserCubit, userId))
enum ProductAction { load, delete, update }
// In Cubit
mix(key: (ProductAction.delete, productId), ...)
// In Widget
context.isWaiting((ProductAction.delete, productId))
// In Cubit - category and subcategory
mix(key: (LoadProducts, categoryId, subcategoryId), ...)
// In Widget
context.isWaiting((LoadProducts, categoryId, subcategoryId))
// In Cubit
mix(key: (action: 'delete', id: productId), ...)
// In Widget
context.isWaiting((action: 'delete', id: productId))
class ProductListItem extends StatelessWidget {
final Product product;
@override
Widget build(BuildContext context) {
final deleteKey = (DeleteProduct, product.id);
return ListTile(
title: Text(product.name),
trailing: context.isWaiting(deleteKey)
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => context.read<ProductCubit>().delete(product.id),
),
);
}
}
class LikeButton extends StatelessWidget {
final String itemId;
final bool isLiked;
@override
Widget build(BuildContext context) {
final key = (ToggleLike, itemId);
if (context.isWaiting(key)) {
return const CircularProgressIndicator();
}
return IconButton(
icon: Icon(isLiked ? Icons.favorite : Icons.favorite_border),
onPressed: () => context.read<ItemCubit>().toggleLike(itemId),
);
}
}
// In Cubit
void toggleLike(String itemId) => mix(
key: (ToggleLike, itemId),
() async {
await api.toggleLike(itemId);
emit(state.copyWith(
likedItems: state.likedItems.contains(itemId)
? state.likedItems.remove(itemId)
: state.likedItems.add(itemId),
));
},
);
class ExpandableItem extends StatelessWidget {
final Item item;
@override
Widget build(BuildContext context) {
final loadKey = (LoadDetails, item.id);
return ExpansionTile(
title: Text(item.name),
onExpansionChanged: (expanded) {
if (expanded && !state.hasDetails(item.id)) {
context.read<ItemCubit>().loadDetails(item.id);
}
},
children: [
if (context.isWaiting(loadKey))
const Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
)
else if (context.isFailed(loadKey))
ListTile(
title: Text('Error: ${context.getException(loadKey)}'),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => context.read<ItemCubit>().loadDetails(item.id),
),
)
else
ItemDetails(details: state.getDetails(item.id)),
],
);
}
}
void processItem(String itemId) => mix(
key: (ProcessItem, itemId),
nonReentrant: nonReentrant, // Inherits key from mix
() async {
await api.process(itemId);
},
);
void refreshItem(String itemId) => mix(
key: (RefreshItem, itemId),
throttle: throttle, // Throttle per item
() async {
final item = await api.getItem(itemId);
emit(state.copyWith(items: {...state.items, itemId: item}));
},
);
void loadUser(String userId) => mix(
key: UserCubit, // State tracking shows ANY user loading
fresh: fresh(
key: (UserCubit, userId), // Freshness tracked per user
freshFor: 5.sec,
),
() async {
final user = await api.getUser(userId);
emit(state.copyWith(users: {...state.users, userId: user}));
},
);
// Widget shows loading for any user
if (context.isWaiting(UserCubit)) { ... }
// Cubit
class CartCubit extends Cubit<CartState> {
CartCubit() : super(const CartState());
void updateQuantity(String itemId, int quantity) => mix(
key: (UpdateQuantity, itemId),
debounce: debounce(duration: 500.millis),
() async {
await api.updateCartItem(itemId, quantity);
emit(state.copyWith(
items: state.items.map((item) =>
item.id == itemId ? item.copyWith(quantity: quantity) : item
).toList(),
));
},
);
void removeItem(String itemId) => mix(
key: (RemoveItem, itemId),
() async {
await api.removeFromCart(itemId);
emit(state.copyWith(
items: state.items.where((i) => i.id != itemId).toList(),
));
},
);
}
// Widget
class CartItemTile extends StatelessWidget {
final CartItem item;
@override
Widget build(BuildContext context) {
final removeKey = (RemoveItem, item.id);
final updateKey = (UpdateQuantity, item.id);
return ListTile(
title: Text(item.name),
subtitle: context.isWaiting(updateKey)
? const Text('Updating...')
: Text('Qty: ${item.quantity}'),
trailing: context.isWaiting(removeKey)
? const CircularProgressIndicator()
: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => context.read<CartCubit>().removeItem(item.id),
),
onTap: () => _showQuantityDialog(context),
);
}
}
key: this for simple single-method scenarios where one loading state is enoughAsk the user: