cobalt_inspector 1.5.0 copy "cobalt_inspector: ^1.5.0" to clipboard
cobalt_inspector: ^1.5.0 copied to clipboard

In-app inspector for Cobalt — the live scope tree, what was built and with what lifetime, and the event log, on a screen inside your app.

The live scope tree Everything the graph reported

cobalt_inspector #

The live scope tree, what Cobalt built and with what lifetime, and everything the graph reported, on a screen inside your app with nothing attached from outside.

flutter pub add dev:cobalt_inspector

Hand the log to the graph when the app starts:

final log = CobaltInspectorLog();

void main() => runApp(
  MaterialApp(
    builder: CobaltAppScope.builder(
      root: const CobaltRoot(),
      observers: [log],
    ),
    home: const HomeScreen(),
  ),
);

Then open the screen from a button in your debug menu:

Navigator.of(context).push(
  MaterialPageRoute<void>(
    builder: (_) => CobaltInspectorScreen(log: log, scope: context.cobaltScope),
  ),
);

Learn more: the three views, your own colours, and the inspector in the Code-Gen guide.

Wiring #

The log has to be installed when the graph is built. Observers are fixed when a scope is constructed and handed down to its children, so an inspector cannot start listening to a graph that is already running:

final log = CobaltInspectorLog();

final scope = await CobaltApplication.start(
  root: const AppScope(),
  observers: [log],
);

Then push the screen from wherever your debug menu lives:

Navigator.of(context).push(
  MaterialPageRoute<void>(
    builder: (_) => CobaltInspectorScreen(log: log, scope: context.cobaltScope),
  ),
);

It climbs from that scope to the root, so it shows the whole graph wherever it opens.

Three views, and why they read different things #

Tree walks the live scopes. It is not rebuilt from events, because an event carries an CobaltScopeRef — a name, a depth and a parent name — and two same-named siblings are indistinguishable there, as are a scope that was disposed and one pushed later under the same name. Good enough to label a log row, not to identify a node.

Each scope lists what it registers with its lifetime, read through registrationOf, and separately what it inherits, with the scope that owns it. That owner is the fact that decides what an override actually affects: a factory runs on the scope that owns its registration, not the one you asked from.

A registration an override replaced is marked overridden, and one a decorator wraps is marked decorated; its sheet names the decorators, innermost first, by the debugLabel they were added with — the annotated class when the container is generated. Both come from overriddenKeys and registrationOf, so marking a row builds nothing.

Built comes from creation events, and has to. A scope's registrations are what was declared — a lazy singleton nobody resolved looks there exactly like one that is built — so only an event proves an object exists. Each row carries how long the build took on its own, without the builds it waited on, with the whole time beside it when they differ; a build whose own time is at least slowBuild — one 60 Hz frame, 16 ms, by default in CobaltInspectorThemeData — is marked, so a class that only waited on a slow dependency is not. A registration's sheet shows how long its last build took, and how much of it without its dependencies.

Log is everything, filterable by event kind.

Two things it will not do #

It never builds anything to show you. Resolving a registration creates it for real: the object starts existing, the scope takes ownership, and a creation event appears. An inspector that resolved rows in order to display them would change the graph it is there to observe. Tapping a row shows facts; building is a separate action that says what it costs.

A value handed to registerSingleton never appears under Built. It is constructed by whoever called it and handed over already made, so the scope has nothing to report constructing. It appears in the tree, with its lifetime, and never in the built list. registerEagerSingleton — and every eager singleton the generator emits — is built by the scope, and appears in both.

The notification is deferred, on purpose #

Observer callbacks are synchronous and arrive in the middle of the work they describe — including a teardown running while the widget tree builds, where notifying immediately throws setState() called during build. CobaltInspectorLog defers to the next turn, and drops a notification that comes due after it has been disposed.

Dressing it in your own colours #

The inspector derives its palette from the host application's Theme, so dropped into an app it already matches — light, dark, whatever seed. Where the derived answer is not the one you want, name the colours:

CobaltInspectorTheme(
  data: CobaltInspectorThemeData.of(Theme.of(context)).copyWith(
    accent: brand.primary,
    failure: brand.danger,
  ),
  child: child,
)

Put it once above wherever a debug menu opens from and pushed routes and sheets inherit it; or pass theme: to a screen to override it there. Nothing is required: with neither, the ambient theme decides.

Colour is by family, not by event: fifteen kinds is more than anyone can hold, and level says how loud an event is rather than what it concerns. The four — scope, startup, instance, failure — are the same division cobalt_talker files its logs under, which is what lets cobalt_talker_flutter dress talker's screen in the same palette.

Three things are drawn from a base colour rather than named directly, and each has an override map beside it. A lifetime badge takes the colour of what retains it, so six lifetimes share three colours; a severity takes failure, warning or muted; a family takes an icon. Where that reading is not yours, name the entries you care about — the map is read per entry, so the rest keep deriving:

CobaltInspectorThemeData.of(Theme.of(context)).copyWith(
  lifetimeColors: {CobaltRegistrationKind.singleton: brand.gold},
  levelColors: {CobaltLogLevel.trace: brand.faint},
  familyIcons: {CobaltInspectorFamily.startup: Icons.bolt},
)

Four opacities decide how strongly a coloured chip reads: tintAlpha for a badge or family mark, selectedTintAlpha and idleTintAlpha for a filter, borderAlpha for the outline. They are settings rather than constants because the same value carries much further on a dark host than on a light one.

Speaking your users' language #

The chrome is translated into English, Russian, Chinese and Korean, and picks the language the host app is already in. Nothing is required for that — with no delegate installed, the inspector reads the ambient Localizations.localeOf and falls back to English for a language it has no translation for. It is a screen you drop in to look at a graph, and asking for a localizationsDelegates edit before it renders at all would be the wrong trade.

Installing the delegate is still the documented path, and the one to take when the app's language is chosen rather than inherited:

MaterialApp(
  localizationsDelegates: const [
    CobaltInspectorL10n.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: CobaltInspectorL10n.supportedLocales,
)

To add a language, drop an .arb beside l10n/inspector_en.arb and run flutter gen-l10n. The two build-time phrases added in 1.4.0 live in lib/src/l10n/build_time_strings.dart instead, so the exported CobaltInspectorL10n keeps its members; add the language there too.

The typeface is the host's too, and that one can go wrong quietly. The inspector sets its text in whatever the ambient Theme provides, which is right — it should look like the app it is inside. But a font is only obliged to have the glyphs it has: a display face chosen for a Latin product often has no Cyrillic and never has CJK, and Flutter then fills those in from the platform. The result is not a crash or a missing-glyph box, it is a change of typeface partway through a line — mid-word where a scope name sits inside a translated sentence. Nothing in a test suite can see it, because widget tests draw in a test font. If you localize an app into a script your display face does not cover, choose a face per language; the gallery in this repository does exactly that, and its README says how.

What stays in English, deliberately. Lifetimes (lazySingleton, asyncSingleton) and levels (trace, warning) are Cobalt's own identifiers — translating them would break the one thing the screen is for, which is finding the line of code a row came from. Scope names and registration keys are your code's, and the framework does not rename it. And the records themselves are written by cobalt, which is pure Dart with no localization in it at all: scope "app/session" pushed is the same sentence in every language. The inspector translates its own chrome and nothing that belongs to somebody else.

What the screens do #

Tree walks the live scopes, with a search over registrations and one control that folds every node. Reading it builds nothing: a lazy singleton nobody asked for is still unbuilt after you have looked at it.

Built comes from creation events, grouped by scope, by lifetime, or not at all — or sorted slowest first by their own time, each row with its build time.

Log is the event stream, newest first, searchable, filtered by family, each row carrying its time and the gap since the one before. Tapping opens the record whole — error, stack and the structured map — with a button that copies it. The pause control in the app bar holds the view still without dropping anything: records keep arriving and appear when you let go.

0
likes
160
points
1.1k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

In-app inspector for Cobalt — the live scope tree, what was built and with what lifetime, and the event log, on a screen inside your app.

Repository (GitHub)
View/report issues
Contributing

Topics

#dependency-injection #di #devtools #debugging

License

MIT (license)

Dependencies

cobalt, cobalt_flutter, flutter, flutter_localizations, intl

More

Packages that depend on cobalt_inspector