Capabilities
Capabilities are metadata tags attached to service registrations. They exist so you can inspect what a service claims to support without instantiating it.
That sounds minor until you start building:
- settings UIs
- extension manifests
- plugin discovery panels
- slot inspectors
- routing logic based on declared behavior
At that point, you definitely want to know what a service claims before you pay the cost of constructing it.
This page comes after services and settings. Once a plugin has something useful to expose, capabilities make that usefulness discoverable to the host app.
The key idea
Section titled “The key idea”Capabilities live on the registration wrapper, not on the service instance.
That means you can inspect them through resolveRaw(...) without constructing the service.
const tooling = Namespace('tooling');final wrapper = context.registry.resolveRaw(tooling('formatter'));final caps = wrapper.capabilities;That makes capabilities safe for discovery code. The host can walk the registry without accidentally starting services, opening sockets, or triggering lazy singletons too early.
A capability can be literally anything
Section titled “A capability can be literally anything”Capability is an empty abstract base class. You subclass it to describe whatever facts about a slot matter to your app. The runtime does not interpret the fields, does not validate them, and does not know which subclasses exist. It just stores them.
That is the point. You are not registering against a fixed schema Plugin Kit defined for you. You are stapling arbitrary tags onto a slot and reading them back later on your own terms.
class SupportsFileFormats extends Capability { /// The set of file extensions this service supports. final Set<String> extensions;
/// Creates a capability declaring support for [extensions]. const SupportsFileFormats(this.extensions);}Any of those is a real capability. All three can coexist on the same registration. The runtime treats them identically.
Attaching capabilities at registration
Section titled “Attaching capabilities at registration”Pass a set of capabilities when you register the service.
class FormatterCapability extends Capability { final List<String> supportedLanguages; const FormatterCapability(this.supportedLanguages);}Later, the host reads them back:
final wrapper = context.registry.resolveRaw(const ServiceId('formatter'));final cap = wrapper.capabilities.getOfType<FormatterCapability>();No instance of SqlFormatter was ever created.
Capability lookup helpers
Section titled “Capability lookup helpers”CapabilityLookup adds two small but useful helpers on Set<Capability>.
hasType<T>()getOfType<T>()
/// Inspects a wrapper for [IsSlowCapability] and warns if slow.void warnIfSlow(PluginContext context) { final wrapper = context.registry.resolveRaw<CodeLinter>( const ServiceId('formatter'), ); final caps = wrapper.capabilities;
if (caps.hasType<IsSlowCapability>()) { final slow = caps.getOfType<IsSlowCapability>()!; if (slow.isSlow) { // Warn the user before invoking this slot in a latency-sensitive path. print('Warning: ${slow.reason}'); } }}Cleaner than manual casting, and clearer at the call site.
void inspectWrapper(ServiceRegistry registry) { final wrapper = registry.resolveRaw<CodeLinter>(const ServiceId('linter')); final caps = wrapper.capabilities; final slow = caps.getOfType<CanBeSlow>(); print('slow reason: ${slow?.reason}'); print('has languages: ${caps.hasType<SupportsLanguages>()}');}Discovery APIs
Section titled “Discovery APIs”| API | What it gives you |
|---|---|
resolveRaw(...) | the current winning wrapper for one slot |
listCapabilitiesOfNamespace(...) | all capabilities seen under a namespace |
Both return data without instantiating services. Combine them with your own hasType<T>() / getOfType<T>() reads to build whatever discovery surface your app needs.
When capabilities are the right tool
Section titled “When capabilities are the right tool”Capabilities work best when they describe stable facts about a slot.
Good examples
- supported languages or file formats
- supported runtimes
- feature tier (“free”, “pro”, “internal”)
- whether the slot is slow or safe to call on the UI thread
Less good examples
- ephemeral runtime state
- data that only makes sense after the service has started
Rule of thumb: if the answer depends on a running instance, it belongs on the service. If the answer should be discoverable before instantiation, it belongs in a capability.