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

Edit objects with UI

Manifold #

Visual editor for immutable Dart objects generated by artifact.

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

What It Does #

  • Renders editable UI from @Property-annotated fields.
  • Supports primitive fields out of the box: String, int, bool, DateTime (nullable and non-nullable).
  • Supports nested artifact objects.
  • Supports List<T> and Set<T> of primitive and artifact types.
  • Supports custom editor overrides by Type.
  • Supports custom field decoration (labels, descriptions, icons, layout).
  • Includes raw model editing dialogs (YAML, JSON, TOML, 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 #

Add Manifold and codegen dependencies to your app:

dependencies:
  manifold: any
  artifact: any

dev_dependencies:
  build_runner: any
  artifact_gen: any

Then run code generation:

dart run build_runner build --delete-conflicting-outputs

Define Models #

import 'package:manifold/manifold.dart';

@manifold
class Species {
  @Property(description: 'Name of species', min: 2, max: 48)
  final String name;

  @Property(description: 'Date discovered')
  final DateTime discoveredAt;

  @Property(description: 'Optional bio')
  final SpeciesBio? bio;

  @Property()
  final List<CharacterBuildingEvent> events;

  @Property()
  final Set<CharacterBuildingEvent> eventsAsSet;

  const Species({
    required this.name,
    required this.discoveredAt,
    this.bio,
    this.events = const [],
    this.eventsAsSet = const {},
  });
}

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

  @Property()
  final DateTime date;

  const CharacterBuildingEvent({required this.name, required this.date});
}

@manifold
class SpeciesBio {
  @Property()
  final String description;

  const SpeciesBio({this.description = ''});
}

Quick Start #

import 'package:arcane/arcane.dart';
import 'package:my_app/gen/artifacts.gen.dart'; // important: 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',
          discoveredAt: DateTime(1900, 1, 1),
        ),
        onChanged: (species) {
          // receives a brand-new immutable instance each edit
          print(species);
        },
      ),
    );
  }
}

How Collection Editing Works #

Manifold treats List<T> and Set<T> similarly:

  • Add item
  • Remove item
  • Edit each item inline or via nested editor

Differences:

  • List<T>: reorderable
  • Set<T>: not reorderable

When adding an artifact item (for example List<Note>), Manifold resolves the generated $AClass<Note> metadata and uses .construct() to create a new default object instance before opening its editor.

Default add-values:

  • String -> ""
  • int -> 0
  • bool -> false
  • DateTime -> DateTime.now()
  • artifact type -> $AClass<T>.construct()
  • unsupported nullable element type -> null

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 applied on top of built-ins, so your overrides replace defaults when type matches.

ManifoldEditor<Species>(
  editorOverrides: {
    String: (ctx) => TextField(
      placeholder: 'Custom string editor',
      onChanged: (v) => ctx.onChanged(v),
    ),

    SpeciesBio: (ctx) => MyBioEditor(
      value: ctx.value as SpeciesBio?,
      onChanged: (v) => ctx.onChanged(v),
    ),

    // Add support for your own non-primitive, non-artifact type.
    Money: (ctx) => MoneyEditor(
      value: ctx.value as Money?,
      onChanged: (v) => ctx.onChanged(v),
    ),
  },
  onChanged: (v) {},
)

ManifoldEditorOverrideContext gives you:

  • field, property
  • value, valueType
  • onChanged
  • collectionElement (true when editing inside list/set)

3) Built-in decorators #

You can choose compact or dense property layout.

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

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

4) Full decorator override: decoratorBuilder #

Wrap/replace the default label-description shell around each editor.

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

Behavior Notes #

  • Only fields annotated with @Property are included.
  • Nested objects can be shown inline using inlineSubObjects: true, or opened as a sub-editor tile.
  • The overflow menu on root editor provides raw edit dialogs:
    • YAML
    • JSON
    • TOML
    • TOON

Troubleshooting #

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

This means artifact reflection for your model type was not registered.

Checklist:

  • Confirm your model has @manifold.
  • Run build runner again.
  • Ensure generated artifacts are imported (for example gen/artifacts.gen.dart) before opening editor.

Minimal API Reference #

ManifoldEditor<T> constructor highlights:

  • edit: initial object value (optional)
  • onChanged: required callback with edited instance
  • editorOverrides: type-based editor override map
  • propertyEditorBuilder: per-field override hook
  • decorator: choose a property decorator implementation
  • decoratorBuilder: direct custom decoration builder
  • inlineSubObjects: render nested objects inline instead of pushing another screen
  • dense: forces dense/compact mode instead of responsive auto mode

Example Project #

See /example for a working app with nested objects, lists, sets, and generated artifact bindings.