문서 목차
이 페이지에서목차

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_events return 0 hits across the repository. What registerBlocInspector() actually registers (package/mcp_inspector/lib/src/inspectors/bloc_inspector.dart) is:

Real toolPurpose
bloc_get_all_logs Recorded observer logs; optional limit / type filter
bloc_get_by_name Logs for one container; blocName required
bloc_get_statsPer-container counts and buffer occupancy
bloc_clearEmpty the buffer (run before reproducing)

The observer is MCPBlocObserver extends BlocObserver, which appends onCreate/onEvent/onChange/onTransition/onError/onClose to a bounded queue. There is no tool that returns live instances or a current state value — read the current value off the last change entry instead. And bloc_signals containers (BlocSignal/CubitSignal) are not Bloc.observer targets 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_signals ships DevToolsBlocSignalObserver + DevToolsService, which registers the VM Service RPC ext.bloc_signal.getInstances (returns each live container's hashCode / 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.observer is 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
  • @flutter-inspector: Master inspector
  • @bloc: BLoC implementation guide
  • @flutter-inspector-ui: Verify state-to-UI connection

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기