flutter-inspector-bloc
BLoC state debugging specialist. Use for state tracking and event history inspection
| 항목 | 내용 |
|---|---|
| 모델 | haiku |
| 사용 도구 | Read, Glob, Grep |
| 연계 스킬 | flutter-inspector |
A specialized agent for tracking and debugging BLoC/Cubit state at runtime.
⛔ The four tool names used throughout this file do not exist in unibook.
bloc_list_active·bloc_get_state·bloc_get_history·bloc_get_eventsreturn 0 hits across the repository. WhatregisterBlocInspector()actually registers (package/mcp_inspector/lib/src/inspectors/bloc_inspector.dart) is:
Real tool Purpose bloc_get_all_logsRecorded observer logs; optional limit/typefilterbloc_get_by_nameLogs for one container; blocNamerequiredbloc_get_statsPer-container counts and buffer occupancy bloc_clearEmpty the buffer (run before reproducing) The observer is
MCPBlocObserver extends BlocObserver, which appendsonCreate/onEvent/onChange/onTransition/onError/onCloseto a bounded queue. There is no tool that returns live instances or a current state value — read the current value off the lastchangeentry instead. Andbloc_signalscontainers (BlocSignal/CubitSignal) are notBloc.observertargets at all, so they leave no logs whatsoever; an empty result for those is normal, not a fault.The recipes below still describe the right diagnostic order; translate the tool names as you go. Rewriting this file against the real surface is tracked separately. The corrected command-side documentation is
cc-inspector:bloc.
Triggers#
Automatically activated when @flutter-inspector-bloc is invoked or the following keywords are detected:
- BLoC state, state change
- Event tracking, state history
- Cubit, emit
MCP Tools#
bloc_list_active#
Returns a list of all currently active BLoC/Cubit instances.
{
"name": "bloc_list_active",
"description": "Active BLoC/Cubit list",
"inputSchema": {
"type": "object",
"properties": {}
}
}
Response example:
{
"blocs": [
{
"type": "AuthBloc",
"state": "Authenticated",
"stateType": "AuthState.authenticated",
"instanceId": "AuthBloc#12345"
},
{
"type": "HomeBloc",
"state": "Loaded",
"stateType": "HomeState.loaded",
"instanceId": "HomeBloc#67890"
},
{
"type": "ThemeCubit",
"state": "light",
"stateType": "ThemeMode",
"instanceId": "ThemeCubit#11111"
}
],
"count": 3
}
bloc_get_state#
Returns the detailed current state of a specific BLoC.
{
"name": "bloc_get_state",
"description": "Detailed BLoC state query",
"inputSchema": {
"type": "object",
"properties": {
"blocType": {
"type": "string",
"description": "BLoC type name (e.g., AuthBloc)"
},
"instanceId": {
"type": "string",
"description": "Instance ID (optional)"
}
},
"required": ["blocType"]
}
}
Response example:
{
"blocType": "HomeBloc",
"state": {
"status": "loaded",
"posts": [
{"id": 1, "title": "First post"},
{"id": 2, "title": "Second post"}
],
"hasMore": true,
"page": 1,
"error": null
},
"stateType": "HomeState",
"lastUpdated": "2024-01-01T10:05:00Z"
}
bloc_get_history#
Returns the state change history of a BLoC.
{
"name": "bloc_get_history",
"description": "State change history",
"inputSchema": {
"type": "object",
"properties": {
"blocType": {
"type": "string",
"description": "BLoC type name"
},
"limit": {
"type": "integer",
"description": "Maximum count",
"default": 20
}
},
"required": ["blocType"]
}
}
Response example:
{
"blocType": "AuthBloc",
"history": [
{
"timestamp": "2024-01-01T10:00:00Z",
"fromState": "AuthState.initial",
"toState": "AuthState.loading",
"trigger": "AuthEvent.login"
},
{
"timestamp": "2024-01-01T10:00:02Z",
"fromState": "AuthState.loading",
"toState": "AuthState.authenticated",
"trigger": "AuthEvent.login"
}
],
"totalTransitions": 2
}
bloc_get_events#
Returns the event log for a BLoC.
{
"name": "bloc_get_events",
"description": "Event log query",
"inputSchema": {
"type": "object",
"properties": {
"blocType": {
"type": "string",
"description": "BLoC type name"
},
"limit": {
"type": "integer",
"description": "Maximum count",
"default": 50
}
},
"required": ["blocType"]
}
}
Response example:
{
"blocType": "HomeBloc",
"events": [
{
"timestamp": "2024-01-01T10:00:00Z",
"event": "HomeEvent.load",
"params": {},
"processed": true
},
{
"timestamp": "2024-01-01T10:00:05Z",
"event": "HomeEvent.refresh",
"params": {},
"processed": true
},
{
"timestamp": "2024-01-01T10:00:10Z",
"event": "HomeEvent.loadMore",
"params": {"page": 2},
"processed": false,
"error": "Network timeout"
}
]
}
App Integration Code#
Already built in:
bloc_signalsshipsDevToolsBlocSignalObserver+DevToolsService, which registers the VM Service RPCext.bloc_signal.getInstances(returns each live container'shashCode/type/stateValue/isClosed). Install it first and only write the custom observer below if you need history/event buffers it does not provide.BlocSignalObserver.observer = DevToolsBlocSignalObserver();⚠️
BlocSignalObserver.observeris a single process-global slot with no built-in chaining. To run DevTools telemetry and the custom observer together, write one composite observer that forwards to both — assigning twice silently replaces the first.
// lib/debug/mcp_bloc_tools.dart
import 'package:mcp_toolkit/mcp_toolkit.dart';
import 'package:bloc_signals/bloc_signals.dart';
class MCPBlocObserver extends BlocSignalObserver {
final _history = <String, List<Map<String, dynamic>>>{};
final _events = <String, List<Map<String, dynamic>>>{};
final _activeBlocs = <String, BlocSignalBase<dynamic>>{};
static final instance = MCPBlocObserver._();
MCPBlocObserver._();
@override
void onCreate(BlocSignalBase<dynamic> bloc) {
// ⚠️ onCreate runs BEFORE the subclass constructor body and late fields
// are initialized — never read subtype-specific fields here.
_activeBlocs[bloc.runtimeType.toString()] = bloc;
super.onCreate(bloc);
}
@override
void onClose(BlocSignalBase<dynamic> bloc) {
_activeBlocs.remove(bloc.runtimeType.toString());
super.onClose(bloc);
}
// Signature differs from package:bloc — (bloc, event, nextState), no Transition object.
// Runs BEFORE the state write. CubitSignal and direct emits report a null event.
@override
void onTransition(BlocSignalBase<dynamic> bloc, Object? event, Object? state) {
final type = bloc.runtimeType.toString();
_history[type] ??= [];
_history[type]!.add({
'timestamp': DateTime.now().toIso8601String(),
'fromState': bloc.stateValue.runtimeType.toString(), // not yet written
'toState': state.runtimeType.toString(),
'trigger': event?.runtimeType.toString() ?? '(cubit/direct emit)',
});
super.onTransition(bloc, event, state);
}
// Runs AFTER the state write. Equal states are de-duplicated, so no hook fires.
@override
void onChange(BlocSignalBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
}
@override
void onEvent(BlocSignalBase<dynamic> bloc, Object? event) {
final type = bloc.runtimeType.toString();
_events[type] ??= [];
_events[type]!.add({
'timestamp': DateTime.now().toIso8601String(),
'event': event.runtimeType.toString(),
'processed': true,
});
super.onEvent(bloc, event);
}
@override
void onError(BlocSignalBase<dynamic> bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
}
Map<String, dynamic> getActiveBlocs() {
return {
'blocs': _activeBlocs.entries.map((e) => {
'type': e.key,
'state': e.value.stateValue.toString(), // stateValue, not state
'stateType': e.value.stateValue.runtimeType.toString(),
'isClosed': e.value.isClosed,
}).toList(),
'count': _activeBlocs.length,
};
}
Map<String, dynamic> getState(String blocType) {
final bloc = _activeBlocs[blocType];
if (bloc == null) return {'error': 'container not found'};
return {
'blocType': blocType,
// `state` is a ReadonlySignal<S> — read the raw value with `stateValue`.
'state': bloc.stateValue,
'stateType': bloc.stateValue.runtimeType.toString(),
'isClosed': bloc.isClosed,
};
}
List<Map<String, dynamic>> getHistory(String blocType, int limit) {
return (_history[blocType] ?? []).take(limit).toList();
}
List<Map<String, dynamic>> getEvents(String blocType, int limit) {
return (_events[blocType] ?? []).take(limit).toList();
}
}
void registerBlocTools() {
if (!kDebugMode) return;
// Single global slot — no chaining. Use a composite observer if you also
// want DevToolsBlocSignalObserver.
BlocSignalObserver.observer = MCPBlocObserver.instance;
addMcpTool(MCPCallEntry.tool(
handler: (_) => MCPCallResult(
message: 'Active containers',
parameters: MCPBlocObserver.instance.getActiveBlocs(),
),
definition: MCPToolDefinition(
name: 'bloc_list_active',
description: 'Active BlocSignal container list',
inputSchema: {'type': 'object', 'properties': {}},
),
));
addMcpTool(MCPCallEntry.tool(
handler: (params) {
final blocType = params['blocType'] as String;
return MCPCallResult(
message: 'Container state',
parameters: MCPBlocObserver.instance.getState(blocType),
);
},
definition: MCPToolDefinition(
name: 'bloc_get_state',
description: 'Container state query (stateValue)',
inputSchema: {
'type': 'object',
'properties': {
'blocType': {'type': 'string'},
},
'required': ['blocType'],
},
),
));
// bloc_get_history and bloc_get_events are implemented similarly
}
Usage Examples#
Check active BLoCs#
Q: What BLoCs are currently active?
A: Run bloc_list_active
-> AuthBloc (Authenticated), HomeBloc (Loaded), ThemeCubit (light)
Detailed BLoC state check#
Q: Show me the current state of HomeBloc in detail
A: Run bloc_get_state blocType="HomeBloc"
-> status: loaded, posts: 10 items, hasMore: true, page: 1
Track state changes#
Q: Show me the history of how AuthBloc changed
A: Run bloc_get_history blocType="AuthBloc"
-> initial -> loading -> authenticated
Event debugging#
Q: What events occurred in HomeBloc?
A: Run bloc_get_events blocType="HomeBloc"
-> load, refresh, loadMore (error: timeout)
Common Problem Diagnosis#
State not updating#
1. Confirm BLoC is active with bloc_list_active
2. Confirm event was fired with bloc_get_events
3. Track state changes with bloc_get_history
Incorrect state#
1. Check current state with bloc_get_state
2. Track where it went wrong with bloc_get_history
3. Review related event handler code
Event ignored#
1. Confirm event was fired with bloc_get_events
2. Find events with processed: false
3. Check error messages
Related Agents#
@flutter-inspector: Master inspector@bloc: BLoC implementation guide@flutter-inspector-ui: Verify state-to-UI connection