agentsclimarketplace

Flutter bloc one shot effects

Skill dtussupbayev/flutter-agentic-skills/skills/flutter-bloc-one-shot-effects

Pattern for one-shot UI effects (SnackBar, navigation, dialog) reacting to BLoC state transitions in flutter_bloc with freezed. Use when adding non-persistent UI reactions tied to a BLoC state change.From its SKILL.md

Install
npx -y skills add dtussupbayev/flutter-agentic-skills --skill flutter-bloc-one-shot-effects

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

4.3 KB, 978 tokens by cl100k_base, as published. Nobody here has run it

Flutter BLoC one-shot UI effects

For SnackBar, navigation after action, toast, dialog triggered by a BLoC state change. Based on the BLoC Todos tutorial (lastDeletedTodo + listenWhen).

Pattern

A nullable payload field on the data-bearing state, plus a BlocListener with listenWhen that fires exactly when the payload changes.

@freezed
sealed class TodosState with _$TodosState {
  const factory TodosState.initial() = TodosInitial;
  const factory TodosState.loading() = TodosLoading;
  const factory TodosState.loaded({
    required List<Todo> todos,
    Todo? lastDeletedTodo,       // entity payload
    DateTime? lastRefreshedAt,   // nonce, when payload can repeat
    Exception? actionError,      // inline error that preserves data
  }) = TodosLoaded;
  const factory TodosState.error({required Exception e}) = TodosError;
}
// Effect-carrying emit: fresh constructor, not copyWith.
// Each emit sets only the relevant marker; other effect fields reset to null.
emit(TodosState.loaded(todos: next, lastDeletedTodo: deleted));
emit(TodosState.loaded(todos: s.todos, actionError: e));

// Data-only update (optimistic UI): copyWith is fine.
// Never set an effect field via copyWith.
emit(s.copyWith(todos: optimisticTodos));
MultiBlocListener(
  listeners: [
    BlocListener<TodosBloc, TodosState>(
      listenWhen: (prev, curr) {
        if (curr is! TodosLoaded || curr.lastDeletedTodo == null) return false;
        if (prev is! TodosLoaded) return true;
        return curr.lastDeletedTodo != prev.lastDeletedTodo;
      },
      listener: (context, state) {
        final s = state as TodosLoaded;
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${s.lastDeletedTodo!.title} deleted')),
        );
      },
    ),
  ],
  child: BlocBuilder<TodosBloc, TodosState>(...),
)

Rules

  • UI effects only inside BlocListener / BlocConsumer.listener. Never in build or BlocBuilder.builder.
  • One listener per effect type, grouped via MultiBlocListener.
  • Effect = nullable payload field on the data-bearing state. Not a separate state variant. Not a sticky boolean.
  • The payload must produce != on every emit that should re-fire the listener. Exception works by identity (new instance every time); plain String does not (value equality). Add a DateTime nonce when equality is value-based.
  • Effect-carrying emits go through a fresh constructor, not copyWith. copyWith carries old markers forward.
  • Builder reads persistent data only. Never check effect fields in build. If a render branch is needed, model it as a regular field with its own name, not as an effect marker.
  • After await, check if (!context.mounted) return;.
  • BLoC / Cubit holds no BuildContext and never calls UI directly.

Terminal state vs payload marker

If the current screen is gone after the effect (Navigator.pop, pushReplacement, sheet close), a payload marker is wrong. Use a separate terminal state variant.

@freezed
sealed class AddItemState with _$AddItemState {
  const factory AddItemState.loaded({...}) = AddItemLoaded;
  const factory AddItemState.success() = AddItemSuccess;  // terminal
  const factory AddItemState.error({required Exception e}) = AddItemError;
}

// listener: no mounted check needed when no await precedes pop
if (state is AddItemSuccess) Navigator.of(context).pop(true);

Checklist

  • UI effects only inside BlocListener / BlocConsumer.listener.
  • Effect is a nullable payload field, not a separate state variant.
  • Payload produces != on every emit (verify equality semantics).
  • All effect-carrying emits go through a fresh constructor.
  • listenWhen checks both the field change and the cross-variant transition.
  • Builder does not read effect fields.
  • After await, context.mounted is checked.

References

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,367. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.