Convert an existing Cubit to use mix() function, removing manual loading and error state management
This skill converts an existing Cubit to use the mix() function, removing manual loading/error state management.
mix()isLoading and errorMessage state managementUserExceptioncontext.isWaiting() and context.isFailed()Ask the user which Cubit they want to convert, or identify Cubits that have:
isLoading field in stateerrorMessage or error field in stateemit(state.copyWith(isLoading: true)) callsBefore (manual state management):
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserState());
Future<void> loadUser() async {
emit(state.copyWith(isLoading: true, errorMessage: null));
try {
final user = await api.getUser();
if (user == null) {
emit(state.copyWith(isLoading: false, errorMessage: 'Failed to load user'));
return;
}
emit(state.copyWith(user: user, isLoading: false));
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
}
}
}
After (with mix):
import 'package:bloc_superpowers/bloc_superpowers.dart';
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserState());
void loadUser() => mix(
key: this,
() async {
final user = await api.getUser();
if (user == null) throw UserException('Failed to load user');
emit(state.copyWith(user: user));
},
);
}
Before:
class UserState {
final User? user;
final bool isLoading;
final String? errorMessage;
UserState({
this.user,
this.isLoading = false,
this.errorMessage,
});
UserState copyWith({
User? user,
bool? isLoading,
String? errorMessage,
}) {
return UserState(
user: user ?? this.user,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
After:
class UserState {
final User? user;
const UserState({this.user});
UserState copyWith({User? user}) {
return UserState(user: user ?? this.user);
}
}
Before (reading state fields):
class UserWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final state = context.watch<UserCubit>().state;
if (state.isLoading) {
return const CircularProgressIndicator();
}
if (state.errorMessage != null) {
return Text('Error: ${state.errorMessage}');
}
return Text('Hello, ${state.user?.name}');
}
}
After (using context extensions):
class UserWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (context.isWaiting(UserCubit)) {
return const CircularProgressIndicator();
}
if (context.isFailed(UserCubit)) {
return Text('Error: ${context.getException(UserCubit)}');
}
final state = context.watch<UserCubit>().state;
return Text('Hello, ${state.user?.name}');
}
}
If a Cubit has multiple methods that need separate loading states, use different keys:
Option A: Same key for all methods (shared loading state)
class UserCubit extends Cubit<UserState> {
void loadUser() => mix(
key: this, // All methods share UserCubit key
() async { ... },
);
void updateUser(User user) => mix(
key: this, // Same key
() async { ... },
);
}
// Widget shows loading for ANY operation:
if (context.isWaiting(UserCubit)) { ... }
Option B: Different keys per method (separate loading states)
enum UserAction { load, update, delete }
class UserCubit extends Cubit<UserState> {
void loadUser() => mix(
key: UserAction.load,
() async { ... },
);
void updateUser(User user) => mix(
key: UserAction.update,
() async { ... },
);
}
// Widget can check specific operations:
if (context.isWaiting(UserAction.load)) { ... }
if (context.isWaiting(UserAction.update)) { ... }
For each Cubit being converted:
import 'package:bloc_superpowers/bloc_superpowers.dart';mix(key: this, () async { ... })emit(state.copyWith(isLoading: true)) callsemit(state.copyWith(isLoading: false)) callsthrow UserException('message')isLoading field from state classerrorMessage/error field from state classcopyWith to remove loading/error parametersstate.isLoading with context.isWaiting(CubitType)state.errorMessage with context.isFailed(CubitType) and context.getException(CubitType)Before:
if (user == null) {
emit(state.copyWith(isLoading: false, errorMessage: 'Not found'));
return;
}
After:
if (user == null) throw UserException('User not found');
Before:
try {
final data = await api.getData();
emit(state.copyWith(data: data, isLoading: false));
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
}
After:
mix(
key: this,
() async {
final data = await api.getData();
emit(state.copyWith(data: data));
},
);
// Errors automatically tracked; UserExceptionDialog shows them
Before:
try {
await api.saveUser(user);
} on NetworkException {
emit(state.copyWith(errorMessage: 'No internet connection'));
} on ValidationException catch (e) {
emit(state.copyWith(errorMessage: e.message));
} catch (e) {
emit(state.copyWith(errorMessage: 'Something went wrong'));
}
After:
mix(
key: this,
catchError: (error, stackTrace) {
if (error is NetworkException) {
throw UserException('No internet connection');
} else if (error is ValidationException) {
throw UserException(error.message);
} else {
throw UserException('Something went wrong');
}
},
() async {
await api.saveUser(user);
},
);
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