| name | adaptive-cards-element-registry |
| description | Use before adding a new Adaptive Card element type โ implementing its widget, registering it, or testing it. Covers the StatefulWidget + mixin pattern, CardTypeRegistry registration, the extension API, and optional packages (charts).
|
Adaptive Card Element Registry Skill
Overview
All Adaptive Card elements are Flutter StatefulWidgets that follow a strict
compositional mixin pattern. New elements are registered in CardTypeRegistry
so the JSON parser can instantiate them by "type" string.
Architecture: Two Registries
CardTypeRegistry โ Elements & Containers
Located in lib/src/registry.dart. Maps JSON "type" strings to element
widgets via two mechanisms:
| Method | Purpose |
|---|
_getBaseElement() / _getBaseAction() | Built-in elements (switch/case) |
addedElements: {} constructor param | Custom/override elements from host apps |
overlayExtensions: [] constructor param | Optional overlay merge hooks (e.g. charts) |
addedActions: {} constructor param | Custom/override actions from host apps |
removedElements: [] constructor param | Suppress specific element types |
ActionTypeRegistry โ Action Handlers
Located in lib/src/action/action_type_registry.dart. Maps JSON action
"type" strings (e.g., "Action.Submit") to handler logic.
There are two complementary sides of the action handling pipeline in this architecture:
-
GenericActions (The "Sender / Processor")
GenericActions (e.g., GenericSubmitAction, GenericExecuteAction) define how an Adaptive Card Action element behaves when a user interacts with it (e.g., tapping a button).
- Role: They encapsulate the internal state logic and payload construction for a specific action type. For example, when an
Action.Submit is tapped, its GenericAction traverses the Flutter widget tree, finds all input fields, validates them, and bundles their values into a data map.
- Responsibility: Form validation, input gathering, state manipulation, and preparing the payload. They are tightly coupled to the Adaptive Card elements.
-
InheritedAdaptiveCardHandlers (The "Receiver / Listener")
InheritedAdaptiveCardHandlers is an InheritedWidget that defines what the host application does once an action has been processed and a payload is ready.
- Role: It acts as an integration point. It allows developers consuming the package to provide application-specific callbacks (
onSubmit, onExecute, onOpenUrl, etc.).
- Responsibility: Executing host application business logic (e.g., making an API call, navigating), completely separated from the internal Adaptive Card UI logic.
How They Work Together: When a user taps an action button, the tap() method on the corresponding GenericAction runs, collecting and validating inputs. Once the payload is ready, the GenericAction looks up the InheritedAdaptiveCardHandlers from the build context and invokes the application-provided callback (like onSubmit(data)), delegating the final execution to the host app.
Implementing a New Built-In Element
Use AdaptiveBadge (lib/src/cards/elements/badge.dart) as the canonical reference
implementation. The pattern for all non-input elements:
Step 1: Create the Widget File
File location: lib/src/cards/elements/my_element.dart
(or lib/src/cards/containers/, lib/src/cards/inputs/ as appropriate)
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_cards_fs/src/adaptive_mixins.dart';
import 'package:flutter_adaptive_cards_fs/src/additional.dart';
import 'package:flutter_adaptive_cards_fs/src/utils/utils.dart';
/// Implements the MyElement Adaptive Card element type.
class AdaptiveMyElement extends StatefulWidget with AdaptiveElementWidgetMixin {
AdaptiveMyElement({
required this.adaptiveMap,
}) : super(key: generateAdaptiveWidgetKey(adaptiveMap)) {
id = loadId(adaptiveMap); // load id before super() via initializer
}
@override
final Map<String, dynamic> adaptiveMap;
@override
late final String id;
@override
AdaptiveMyElementState createState() => AdaptiveMyElementState();
}
class AdaptiveMyElementState extends State<AdaptiveMyElement>
with AdaptiveElementMixin, AdaptiveVisibilityMixin {
// Declare fields parsed from adaptiveMap
late String text;
@override
void initState() {
super.initState();
// Parse JSON properties here โ all strings are nullable from JSON
text = adaptiveMap['text'] as String? ?? '';
}
@override
Widget build(BuildContext context) {
return Visibility(
visible: isVisible, // from AdaptiveVisibilityMixin
child: SeparatorElement( // handles spacing/separator JSON properties
adaptiveMap: adaptiveMap,
child: Text(text),
),
);
}
}
Step 2: Register in CardTypeRegistry
File: lib/src/registry.dart โ add to the import block and the switch:
// 1. Add import at the top
import 'package:flutter_adaptive_cards_fs/src/cards/elements/my_element.dart';
// 2. Add a case in _getBaseElement():
case 'MyElement':
return AdaptiveMyElement(adaptiveMap: map);
Step 3: Export from the Extension Library (if needed for consumers)
If consumers need to subclass or reference your element, add it to:
lib/flutter_adaptive_cards_extend.dart
Mixin Reference
Mixins provide shared behavior. Apply them to the State class:
| Mixin | Applied to | Provides |
|---|
AdaptiveElementWidgetMixin | StatefulWidget | adaptiveMap, id abstract getters |
ProviderScopeMixin<T> | State<T> | cardTypeRegistry, actionTypeRegistry, styleResolver, rawRootCardWidgetState, adaptiveCardElementState |
AdaptiveElementMixin<T> | State<T> | id, style, adaptiveMap (combine with ProviderScopeMixin for registries/resolver) |
AdaptiveVisibilityMixin<T> | State<T> | isVisible, setIsVisible() โ listens to resolvedElementProvider(id) for merged "isVisible" |
AdaptiveActionMixin<T> | State<T> | title, tooltip โ for action widgets |
AdaptiveInputMixin<T> | ConsumerState<T> | watchResolvedInput() / readResolvedInput(), setDocumentInputValue(), setLocalValidationError() / clearLocalValidationError(), listenForResolvedValueChanges(); appendInput(), resetInput(), etc. No cached overlay mirrors. |
Typical element (non-input):
with AdaptiveElementMixin, AdaptiveVisibilityMixin, ProviderScopeMixin
Typical input element (ConsumerStatefulWidget / ConsumerState):
with AdaptiveElementMixin, AdaptiveVisibilityMixin, AdaptiveInputMixin, ProviderScopeMixin
In build(): listenForResolvedValueChanges(); final input = watchResolvedInput(); โ use input.label, input.isRequired, etc. Imperative paths (checkRequired, resetInput overrides): readResolvedInput().
Typical action widget:
with AdaptiveElementMixin, AdaptiveActionMixin, ProviderScopeMixin
Runtime state: baseline + overlays
Card JSON is deep-copied into a baseline at render time. Runtime changes (input values, visibility, TextBlock text, validation, ChoiceSet choices) are stored in sparse overlaysById entries โ the host map is never mutated. Action isEnabled uses actionOverlaysById + resolvedActionProvider(id).
| Phase | Source |
|---|
| Initial UI | resolvedElementProvider(id) (baseline until overlays are written) |
| User edits / ToggleVisibility | AdaptiveCardDocumentNotifier โ overlaysById[id] |
| What inputs read in build | watchResolvedInput() โ merged baseline + overlay |
Inputs: call setDocumentInputValue(value) when the user changes the field; implement onDocumentValueChanged to sync controllers when resolved value changes (reset, initData). Use watchResolvedInput() in build() for label / placeholder / validation / required โ do not cache overlay fields on the mixin. Do not call setState from initInput โ write the overlay and let ref.watch / ref.listen rebuild (see docs/reactive-riverpod.md).
ChoiceSet: subscribe to resolved choices (see AdaptiveChoiceSet); host-driven updates use setChoices / appendChoices or RawAdaptiveCardState.loadInput.
Visibility: call setIsVisible(visible: โฆ) or rely on Action.ToggleVisibility; AdaptiveVisibilityMixin listens to resolved isVisible.
TextBlock: host-driven copy changes use setText / clearText on the document notifier (or RawAdaptiveCardState); AdaptiveTextBlock listens to resolved text โ do not mutate adaptiveMap['text'] in place.
Validation (inputs): All validation display uses resolved isInvalid / errorMessage. Host code: setInputError / clearInputError. Form validators and checkRequired: setLocalValidationError() / clearLocalValidationError() on AdaptiveInputMixin. Input.Text supports baseline regex (validated in the Form validator and on Submit via validateInputs()). User edits (setInputValue) and factory reset clear validation overlays.
Actions (isEnabled): setActionEnabled + AdaptiveActionStateMixin / resolvedActionProvider โ not ElementOverlay.
Submit / reset: collectInputValues() and resetAllInputs() / resetInput(id) on the document notifier โ do not walk the widget tree. Factory reset clears input overlays including label, placeholder, and isRequired (resolved โ baseline); preserves input isVisible and typeahead session fields. See docs/reactive-riverpod.md.
Full detail: overlay-properties-by-type.md, reactive-riverpod.md.
Overlay test coverage
What overlay behavior is tested vs. still a gap โ the notifier/widget coverage
matrix, cross-cutting concerns, and the checklist for adding tests to a new
overlay field โ lives in
references/overlay-test-coverage.md.
Consult it when adding a new overlay field or deciding whether a specific
Input.* / Action.* type needs a widget test.
Key Generation (Widget Keys)
Every element must set its key deterministically from adaptiveMap. The
generateAdaptiveWidgetKey function handles this automatically:
// In StatefulWidget constructor:
AdaptiveMyElement({required this.adaptiveMap})
: super(key: generateAdaptiveWidgetKey(adaptiveMap)) {
id = loadId(adaptiveMap);
}
This produces:
- Widget key:
ValueKey('${id}_adaptive')
- Child content key (for inputs):
ValueKey('$id') or ValueKey('${id}_suffix')
Tests use these keys to locate widgets:
find.byKey(const ValueKey('myElementId_adaptive')) // outer StatefulWidget
find.byKey(const ValueKey('myElementId')) // inner content widget
Accessing HostConfig (Theme/Style)
From within a State's build() method, read the ReferenceResolver to apply
theme-aware colors, font sizes, and spacing:
// In element State with ProviderScopeMixin:
final resolver = styleResolver;
final Color foreground = resolver.resolveContainerForegroundColor(
style: style ?? 'default',
isSubtle: false,
);
final double fontSize = resolver.resolveFontSize(
context: context,
sizeString: adaptiveMap['size']?.toString() ?? 'default',
);
// ...
Note: Shared services (registries, resolver, card state) come from
ProviderScopeMixin, which reads card-scoped Riverpod providers installed by
RawAdaptiveCard / AdaptiveCardElement. See
reactive-riverpod.md.
Optional extension packages (charts)
Chart elements and chart runtime overlays belong in flutter_adaptive_charts_fs, not in the core library.
Core boundary (flutter_adaptive_cards_fs)
- Avoid chart-specific classes, imports, overlay fields, and merge logic in core (
chartData, ChartProperties, fl_chart, etc.).
- Use generic extension hooks:
CardTypeRegistry.addedElements โ register optional element widgets by JSON "type"
CardTypeRegistry.overlayExtensions โ register ElementOverlayExtension implementations
ElementOverlay.extensionPayloads โ opaque per-extension overlay storage
RawAdaptiveCardState.patchExtensionOverlay(...) โ host/runtime patches for extensions
Charts package (flutter_adaptive_charts_fs)
- Register widgets:
CardChartsRegistry.additionalChartElements
- Register overlay behavior:
CardChartsRegistry.overlayExtensions (e.g. ChartElementOverlayExtension)
- Chart overlay host helpers live on extensions in the charts package (e.g.
ChartOverlayHost on RawAdaptiveCardState), not in core.
import 'package:flutter_adaptive_charts_fs/flutter_adaptive_charts_fs.dart';
final registry = CardTypeRegistry(
addedElements: CardChartsRegistry.additionalChartElements,
overlayExtensions: CardChartsRegistry.overlayExtensions,
);
When adding overlay coverage for chart elements, put tests under packages/flutter_adaptive_charts_fs/test/ and pass a registry that includes both additionalChartElements and overlayExtensions.
See docs/optional-packages-and-extensions.md.
Custom Elements in Host Apps (Extension API)
Host applications can register custom or override elements without modifying
the library, using CardTypeRegistry.addedElements:
// In the host app:
import 'package:flutter_adaptive_cards_fs/flutter_adaptive_cards_fs.dart';
import 'package:flutter_adaptive_cards_fs/flutter_adaptive_cards_fs_extend.dart';
final registry = CardTypeRegistry(
addedElements: {
'MyCustomElement': (map) => MyCustomWidget(adaptiveMap: map),
'TextBlock': (map) => MyOverrideTextBlock(adaptiveMap: map), // override
},
removedElements: ['Media'], // disable an element type
);
AdaptiveCardsCanvas.asset(
assetPath: 'assets/my_card.json',
cardTypeRegistry: registry,
hostConfigs: HostConfigs(),
);
The extension library re-exports the mixins and utilities a custom element
needs:
import 'package:flutter_adaptive_cards_fs/flutter_adaptive_cards_fs_extend.dart';
// Gives access to: AdaptiveElementWidgetMixin, AdaptiveElementMixin,
// AdaptiveVisibilityMixin, SeparatorElement, generateAdaptiveWidgetKey,
// generateWidgetKey, loadId, etc.
Testing a New Element
If the element reads runtime state via resolvedElementProvider (visibility, input value, validation, text, etc.), add notifier unit tests and at least one focused widget test for that overlay field. Do not assume other element types are covered โ see Overlay test coverage.
-
Create a sample JSON in packages/flutter_adaptive_cards_fs/test/samples/:
{
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "MyElement",
"id": "myElem1",
"text": "Hello"
}
]
}
-
Write a widget test using the standard test helpers:
import 'utils/test_utils.dart';
testWidgets('MyElement renders text', (tester) async {
await tester.pumpWidget(
getTestWidgetFromPath(path: 'my_element_test.json'),
);
await tester.pumpAndSettle();
expect(find.text('Hello'), findsOneWidget);
});
-
Add a golden test (see adaptive-cards-testing skill).
-
Run tests from the package directory:
cd packages/flutter_adaptive_cards_fs
fvm flutter test
-
Optional Widgetbook demo: add JSON under widgetbook/lib/samples/, register new directories in widgetbook/pubspec.yaml (flutter: assets:), add a @widgetbook.UseCase, and run fvm dart run build_runner build in widgetbook/. See widgetbook/README.md.