kell_creations/kell_creations_apps/docs/composition-strategy.md

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:

  1. Read runtime configuration from --dart-define values.
  2. Construct environment-appropriate services (fake vs. real backends).
  3. 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 use BottomNavigationBar or 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.