8.3 KiB
Kell Creations — Cross-Platform Shell Composition Strategy
Overview
This document describes the shared composition pattern that all Kell Creations
app targets (kell_web, kell_mobile, future targets) must follow. The pattern
lives in the core package and provides a consistent way to:
- Read runtime configuration from
--dart-definevalues. - Construct environment-appropriate services (fake vs. real backends).
- Expose services and configuration to the widget tree.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ core (shared package) │
│ │
│ KcAppConfig / KcAppEnvironment │
│ └─ Runtime config from --dart-define │
│ │
│ KcAppServices (abstract) │
│ └─ Base class for app service containers │
│ │
│ KcServiceFactory<T extends KcAppServices> │
│ └─ Generic factory: createFake() / createWordPress(cfg) │
│ │
│ KcBootstrap │
│ └─ Shared bootstrap: env switch + WP credential fallback │
│ │
│ KcAppScope<T extends KcAppServices> │
│ └─ InheritedWidget exposing T + KcAppConfig to tree │
└──────────────────────────────────────────────────────────────┘
▲ ▲
│ │
┌────────┴───────────┐ ┌────────────┴──────────────┐
│ kell_web (app) │ │ kell_mobile (app) │
│ │ │ │
│ AppServices │ │ MobileAppServices │
│ extends │ │ extends │
│ KcAppServices │ │ KcAppServices │
│ │ │ │
│ AppScope │ │ (uses KcAppScope │
│ extends │ │ <MobileAppServices>) │
│ KcAppScope │ │ │
│ <AppServices> │ │ │
│ │ │ │
│ Bootstrap │ │ (uses KcBootstrap.run │
│ delegates to │ │ with own factory) │
│ KcBootstrap.run │ │ │
└─────────────────────┘ └────────────────────────────┘
Why abstract services?
Concrete service containers hold references to feature-package repository types
(e.g., InventoryRepository, ProductPublishingRepository). Moving those
references into core would create circular dependencies:
core → feature_wordpress → core ← NOT ALLOWED
By keeping KcAppServices abstract in core, the package owns the composition
contract without knowing concrete types. Each app provides its own concrete
subclass.
Contract for new app targets
When creating a new app target (e.g., kell_mobile), follow this pattern:
1. Create a concrete services class
// apps/kell_mobile/lib/composition/mobile_app_services.dart
import 'package:core/core.dart';
import 'package:feature_inventory/feature_inventory.dart';
import 'package:feature_wordpress/feature_wordpress.dart';
// ... other feature imports
class MobileAppServices extends KcAppServices {
final InventoryRepository inventoryRepository;
final ProductPublishingRepository productPublishingRepository;
// ... other repositories as needed
const MobileAppServices({
required this.inventoryRepository,
required this.productPublishingRepository,
});
factory MobileAppServices.fake() {
return MobileAppServices(
inventoryRepository: FakeInventoryRepository(),
productPublishingRepository: FakeProductPublishingRepository(),
);
}
factory MobileAppServices.wordpress({
required String siteUrl,
required String consumerKey,
required String consumerSecret,
}) {
final apiClient = WooCommerceApiClient(
siteUrl: siteUrl,
consumerKey: consumerKey,
consumerSecret: consumerSecret,
);
return MobileAppServices(
inventoryRepository: FakeInventoryRepository(),
productPublishingRepository:
WordPressProductPublishingRepository(apiClient: apiClient),
);
}
static KcServiceFactory<MobileAppServices> get serviceFactory {
return KcServiceFactory<MobileAppServices>(
createFake: () => MobileAppServices.fake(),
createWordPress: (config) => MobileAppServices.wordpress(
siteUrl: config.wcSiteUrl,
consumerKey: config.wcConsumerKey,
consumerSecret: config.wcConsumerSecret,
),
);
}
}
2. Wire up main.dart
// apps/kell_mobile/lib/main.dart
import 'package:core/core.dart';
import 'package:flutter/material.dart';
import 'composition/mobile_app_services.dart';
void main() {
final config = KcAppConfig.fromEnvironment();
final (:services, config: effectiveConfig) = KcBootstrap.run(
config,
MobileAppServices.serviceFactory,
);
runApp(
KcAppScope<MobileAppServices>(
services: services,
config: effectiveConfig,
child: const KellMobileApp(),
),
);
}
3. Access services in widgets
// Anywhere in the widget tree:
final services = KcAppScope.of<MobileAppServices>(context);
final config = KcAppScope.configOf<MobileAppServices>(context);
Backward compatibility (kell_web)
The kell_web app retains its original class names (AppConfig, AppScope,
Bootstrap, AppServices) as thin wrappers/typedefs around the shared core
types. Existing code continues to work without modification:
| Original (kell_web) | Shared (core) | Relationship |
|---|---|---|
AppConfig |
KcAppConfig |
typedef |
AppEnvironment |
KcAppEnvironment |
typedef |
AppServices |
KcAppServices |
extends |
AppScope |
KcAppScope |
extends (typed) |
Bootstrap |
KcBootstrap |
delegates |
New code in kell_web may use either the original names or the shared Kc-prefixed
names. The shared names are preferred for clarity and consistency with kell_mobile.
Runtime configuration
All app targets use the same --dart-define keys:
| Key | Description | Default |
|---|---|---|
KC_ENV |
fake or wordpress |
fake |
KC_WC_SITE_URL |
WordPress site URL | (empty) |
KC_WC_CONSUMER_KEY |
WooCommerce REST API consumer key | (empty) |
KC_WC_CONSUMER_SECRET |
WooCommerce REST API consumer secret | (empty) |
When KC_ENV=wordpress but credentials are missing, KcBootstrap automatically
falls back to fake mode with a debug-mode warning.
What remains app-specific
The following concerns are not shared and remain in each app target:
- App shell / navigation — Web uses
NavigationRail, mobile will useBottomNavigationBaror similar. - Routing — Route definitions and page builders are platform-specific.
- Platform-specific presentation — Layout, responsive breakpoints, etc.
- Cross-feature navigation handoffs — Wired in the app's routing layer.
The design_system package provides shared visual components (widgets, theme,
typography, breakpoints) that both web and mobile can use for consistent styling.