buildAdapter<T extends NutrientController> static method

T buildAdapter<T extends NutrientController>()

Build the controller for a NutrientDocumentView<T>.

Resolution order:

  1. a builder registered via addAdapterClass for T (fresh instance);
  2. when no specific type was requested (T == NutrientController, i.e. a bare NutrientDocumentView()), a fresh platform default viewer per view — resolved before the shared slot so concurrent bare views never collide on one controller;
  3. otherwise, a typed instance registered through the deprecated global slots (initialize's androidAdapter:/etc.) when it matches T.

Throws StateError with an actionable message when T is a specific type with no registration — surfacing the misconfiguration instead of a silent null. Intended for the SDK's views; apps use addAdapterClass + NutrientDocumentView<T>.

Implementation

static T buildAdapter<T extends NutrientController>() {
  final factory = _factories[T];
  if (factory != null) return factory() as T;

  // No specific controller type requested (a bare `NutrientDocumentView()`):
  // build a FRESH platform default viewer per view. Resolve this BEFORE the
  // shared global slot so two simultaneous bare views each own their own
  // controller (multi-view safe + disposed by the view) instead of clobbering
  // the one process-shared default — the A1 collision this design prevents.
  if (T == NutrientController) {
    final fallback = _makeDefaultAdapter();
    if (fallback is T) return fallback as T;
  }

  // Same, for a bare `NutrientInstantView()`: build a FRESH platform default
  // Instant controller per view (implements NutrientInstantController, so the
  // Instant sync methods are available on the surfaced controller).
  if (T == NutrientInstantController) {
    final fallback = _makeDefaultInstantAdapter();
    if (fallback is T) return fallback as T;
  }

  // Back-compat: a *typed* controller registered through the (deprecated)
  // global slots — only reached for a concrete T (a bare view took the
  // fresh-default path above). The slot's static type (NutrientPlatformAdapter)
  // is unrelated to T so `is T` can't promote; the real adapters implement
  // both, so the runtime check + cast is sound.
  final slot = _adapterSlotForCurrentPlatform();
  if (slot is T) return slot as T;

  throw StateError(
    'No adapter registered for $T. Pass `adapter:` to NutrientDocumentView, '
    'or register a factory with Nutrient.addAdapterClass<$T>(() => ...).',
  );
}