manifold 1.0.0
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>andSet<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.7in 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>: reorderableSet<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->0bool->falseDateTime->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,propertyvalue,valueTypeonChangedcollectionElement(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
@Propertyare 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 instanceeditorOverrides: type-based editor override mappropertyEditorBuilder: per-field override hookdecorator: choose a property decorator implementationdecoratorBuilder: direct custom decoration builderinlineSubObjects: render nested objects inline instead of pushing another screendense: 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.