Building a Studio host
The Studio is storage-agnostic. To embed it in your app, you wire four small ports against your backend and mount one widget.
The contract
import 'package:pose_flow_studio/pose_flow_studio.dart';
StudioScope( persistence: yourPersistence, // required assetBasePath: 'pose_flow/', // optional (web only) metadataPanelBuilder: (ctx, id) => ..., // optional child: const PoseFlowStudioScreen(),)That’s the entire surface. StudioScope is an InheritedWidget;
the Studio reads its props via StudioScope.of(context),
StudioScope.assetBasePathOf(context), and
StudioScope.metadataPanelBuilderOf(context).
Step 1. Implement the four ports
class MyStudioCatalog implements StudioCatalog { // Load + save Movement instances. @override Future<Movement?> load(String id) async { ... } @override Future<void> save(Movement movement, ...) async { ... } @override Future<List<MovementSummary>> listSummaries(...) async { ... } @override String export(Movement movement) => ...;}
class MyStudioAudit implements StudioAudit { @override Future<void> log(StudioAuditEvent event) async { ... }}
class MyStudioPhaseRepository implements StudioPhaseRepository { @override Future<void> replaceAll(...) async { ... } @override Future<List<PhaseDefinition>> list(...) async { ... } @override Future<void> deleteForMovement(...) async { ... }}
class MyStudioVersionRepository implements StudioVersionRepository { @override Future<void> snapshot(...) async { ... } @override Future<List<MovementSnapshot>> list(...) async { ... }}For backends where some ports don’t make sense (e.g. no audit log), implement as a no-op, the Studio doesn’t check return values, only that the call completes.
See persistence for the full interface contracts.
Step 2. Bundle them
StudioPersistence buildPersistence() { return StudioPersistence( catalog: MyStudioCatalog(), audit: MyStudioAudit(), phases: MyStudioPhaseRepository(), versions: MyStudioVersionRepository(), );}Step 3. Mount the Studio
import 'package:flutter/material.dart';import 'package:pose_flow_studio/pose_flow_studio.dart';
class StudioRoute extends StatelessWidget { const StudioRoute({super.key, this.movementId});
final String? movementId;
@override Widget build(BuildContext context) { return StudioScope( persistence: buildPersistence(), assetBasePath: 'pose_flow/', child: PoseFlowStudioScreen(movementId: movementId), ); }}movementId is optional, null opens an empty draft, a value
hydrates the editor with the existing movement.
Step 4. Web assets (if shipping on web)
Copy the five pose-engine assets into your web app’s web/
directory. See web integration.
Quick-start with in-memory persistence
For development or a no-backend install:
final persistence = InMemoryStudioPersistence().bundle();
StudioScope( persistence: persistence, child: const PoseFlowStudioScreen(),);In-memory persistence ships with the Studio package, useful for demos, tests, and as a reference implementation when wiring your own.
Optional, the metadata panel hook
The Studio’s left column has a slot for a host-provided metadata panel (body parts, muscle groups, image URLs, fields specific to your host’s domain).
StudioScope( persistence: persistence, metadataPanelBuilder: (context, movementId) { return YourMetadataPanel(movementId: movementId); }, child: const PoseFlowStudioScreen(),)The Studio renders your widget inside a FloatingPanel above the Phases panel. When the builder is null (or returns null), the slot is hidden.
The typical pattern: your CMS owns the host-side metadata (image / video URLs, body parts, muscle groups, etc.) and surfaces it through a panel widget. The Studio doesn’t know what those fields are; it just gives the slot.
Reference patterns
Two persistence shapes most hosts pick from:
- In-memory + a local SQL store (e.g. Drift / SQLite). No cloud, full-feature, useful for offline-friendly trainer authoring.
- Firestore + Cloud Storage (or any cloud-backed equivalent). Multi-user, audited, the right shape when authoring happens inside a managed CMS.
Either can serve as a starting point, the StudioPersistence
interface doesn’t care which.
Routing
The Studio doesn’t impose any router on you. Use go_router,
the built-in Navigator, anything, push the route, mount the
widget, you’re done.
For deep links into specific movements, expose
PoseFlowStudioScreen(movementId: '...') from your router.
Testing
The Studio is testable in isolation with InMemoryStudioPersistence:
testWidgets('open studio with a movement loaded', (tester) async { final persistence = InMemoryStudioPersistence(); await persistence.bundle().catalog.save(seededMovement, domain: 'fitness');
await tester.pumpWidget(MaterialApp( home: StudioScope( persistence: persistence.bundle(), child: PoseFlowStudioScreen(movementId: seededMovement.id), ), )); // ... assertions});The Studio’s own widget tests use this pattern.
Camera lifecycle
The Studio creates + owns its own camera + tracker. You don’t need to plumb a camera in; just mount the screen.
For native targets the platform camera plugin is auto-initialised. For web, the WASM worker initialises on first frame request.
Read next
- Persistence, port contracts.
- Studio overview.
- Authoring workflow.
- Web integration, for asset path setup.