manifold 1.0.1 copy "manifold: ^1.0.1" to clipboard
manifold: ^1.0.1 copied to clipboard

Edit objects with UI

Manifold #

Visual editor for immutable Dart objects generated by artifact.

Manifold uses artifact reflection metadata to build forms automatically, edit values, and emit a new object instance on every change.

Highlights #

  • Auto-generates editors from @Property fields.
  • Supports primitives: String, int, double, bool, DateTime (nullable and non-nullable).
  • Supports enum fields (nullable and non-nullable).
  • Supports nested artifact objects.
  • Supports recursive List, Set, and Map trees with mixed nesting.
    • Example: Map<String, List<Map<int, Map<double, ASubObject>>>>
  • Supports custom editor overrides by Type (user overrides replace defaults).
  • Supports custom decorators and full decorator override.
  • Supports container policy (embed vs subScreen) for collections, maps, and sub-objects.
  • Supports live search filtering.
  • Supports read-only/view mode.
  • Includes raw model dialogs for YAML, JSON, TOML, and TOON.

Requirements #

  • Flutter + Dart SDK compatible with this package (sdk: ^3.10.7 in this repo).
  • Models generated by artifact/artifact_gen.
  • Fields you want visible in the editor must have @Property.

Install #

dependencies:
  manifold: any
  artifact: any

dev_dependencies:
  build_runner: any
  artifact_gen: any

Then run codegen:

dart run build_runner build --delete-conflicting-outputs

Define Models #

import 'package:manifold/manifold.dart';

enum EventType { build, release, test }

@manifold
class Note {
  @Property()
  final String text;

  const Note({this.text = ''});
}

@manifold
class Species {
  @Property()
  final String name;

  @Property()
  final double rating;

  @Property()
  final EventType latestEvent;

  @Property()
  final List<Note> notes;

  @Property()
  final Set<EventType> eventTypes;

  @Property()
  final Map<String, List<Note>> notesByCategory;

  const Species({
    required this.name,
    required this.rating,
    required this.latestEvent,
    this.notes = const [],
    this.eventTypes = const {},
    this.notesByCategory = const {},
  });
}

Quick Start #

import 'package:arcane/arcane.dart';
import 'package:my_app/gen/artifacts.gen.dart'; // registers artifact accessors
import 'package:my_app/models.dart';
import 'package:manifold/editor.dart';

class SpeciesEditorScreen extends StatelessWidget {
  const SpeciesEditorScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Screen(
      child: ManifoldEditor<Species>(
        edit: Species(
          name: 'Pigeon',
          rating: 4.2,
          latestEvent: EventType.build,
        ),
        onChanged: (species) {
          print(species); // new immutable instance every edit
        },
      ),
    );
  }
}

Collections And Maps #

Manifold supports add/remove/edit for recursive collections:

  • List<T>: reorderable
  • Set<T>: not reorderable
  • Map<K, V>: key/value entry editor with unique-key enforcement

Collection and map values are materialized using artifact $AT descriptors, so deep nested structures stay strongly typed.

When adding artifact values (for example List<Note> or Map<String, Note>), Manifold resolves $AClass<Note> and calls .construct().

Default add values:

  • String -> ""
  • int -> 0
  • double -> 0.0
  • bool -> false
  • DateTime -> DateTime.now()
  • enum -> first inferred enum option
  • artifact -> $AClass<T>.construct()
  • nullable target -> null

Read-Only Mode #

Use viewOnly: true to disable edits across the entire tree. Field controls are disabled and provider pushes are ignored.

ManifoldEditor<Species>(
  viewOnly: true,
  onChanged: (_) {},
)

Customization #

1) Per-field early override: propertyEditorBuilder #

Runs before default behavior. Return null to fall back.

ManifoldEditor<Species>(
  propertyEditorBuilder: (ctx) {
    if (ctx.field.name == 'name') {
      return TextField(
        placeholder: 'Species name',
        onChanged: (v) => ctx.onChanged(v),
      );
    }
    return null;
  },
  onChanged: (v) {},
)

2) Type override map: editorOverrides #

User map is layered on top of built-ins, so matching keys replace defaults.

ManifoldEditor<Species>(
  editorOverrides: {
    String: (ctx) => TextField(
      placeholder: 'Custom string editor',
      onChanged: (v) => ctx.onChanged(v),
    ),
    Note: (ctx) => MyNoteEditor(
      value: ctx.value as Note?,
      onChanged: (v) => ctx.onChanged(v),
    ),
  },
  onChanged: (v) {},
)

ManifoldEditorOverrideContext includes:

  • field, property
  • value, valueType
  • onChanged
  • collectionElement
  • readOnly

3) Decorators #

Choose a built-in decorator:

import 'package:manifold/decorator/compact_property_decorator.dart';

ManifoldEditor<Species>(
  decorator: const ManifoldCompactPropertyDecorator(),
  onChanged: (v) {},
)

Or fully override with decoratorBuilder:

ManifoldEditor<Species>(
  decoratorBuilder: (ctx) {
    return Card(
      titleText: ctx.label,
      subtitleText: ctx.property?.description,
      child: ctx.editor,
    );
  },
  onChanged: (v) {},
)

4) Container style (embed vs subScreen) #

Use containerStyle to decide layout per field/value/type for:

  • getCollectionStyle
  • getMapStyle
  • getSubObjectStyle
class MyContainerStyle extends ManifoldContainerStyle {
  const MyContainerStyle();

  @override
  ManifoldContainerType getCollectionStyle<M, T>({
    required ManifoldEditorScope<M> scope,
    required $AFld field,
    required Iterable<T>? value,
  }) {
    if ((value?.length ?? 0) > 4) {
      return ManifoldContainerType.subScreen;
    }
    return ManifoldContainerType.embed;
  }

  @override
  ManifoldContainerType getMapStyle<M, K, V>({
    required ManifoldEditorScope<M> scope,
    required $AFld field,
    required Map<K, V>? value,
  }) {
    return ManifoldContainerType.embed;
  }

  @override
  ManifoldContainerType getSubObjectStyle<M, O>({
    required ManifoldEditorScope<M> scope,
    required $AFld field,
    required O? value,
  }) {
    return ManifoldContainerType.subScreen;
  }
}

Raw Editors #

Root overflow menu includes:

  • YAML
  • JSON
  • TOML
  • TOON

If a format cannot encode the current object graph (for example TOML with null values), that format is shown as unavailable instead of crashing the editor.

Behavior Notes #

  • Only @Property fields are rendered.
  • Live search filters the visible fields.
  • Collection/map element identity is tracked to reduce focus loss and animation churn during updates/reorder/search.
  • For fire_crud-style models, documentPath is preserved across edits.

Troubleshooting #

"No artifact accessor found for type ..." #

Artifact reflection for the model type was not registered.

Checklist:

  • Confirm model has @manifold.
  • Run build_runner again.
  • Import generated artifacts (for example gen/artifacts.gen.dart) before opening ManifoldEditor.

Minimal API Reference #

ManifoldEditor<T> primary parameters:

  • edit: initial object (optional)
  • onChanged: required edited instance callback
  • viewOnly: disables editing
  • editorOverrides: type-based override map
  • propertyEditorBuilder: per-field early override hook
  • decorator: choose decorator implementation
  • decoratorBuilder: direct decoration override
  • containerStyle: choose embed/subScreen for collections/maps/sub-objects
  • inlineSubObjects: force nested object embedding
  • dense: force dense/compact layout instead of responsive auto mode

Example Project #

See /example for a working app with:

  • nested artifact objects
  • enums and doubles
  • recursive lists/sets
  • nested maps mixed with collections