Rows are addressed by stable clientId, never by their changing index. The interactive example lets you drag rows, edit them, and add or remove entries; its panel shows each id moving with its row.
0 17258440… Passport 1 c6a7f3c6… Charger 2 ca448623… Sunscreen
The example source#
These are the example app's own files—the interactive demo uses the same schema and controller:
@keyedSchema
library;
import 'package:keyed_form_schema/keyed_form_schema.dart';
part 'packing_schema.kfg.dart';
final _packingSchema = ks.object({
'items': ks
.list(
ks.object(className: 'PackingItemSchema', {
'label': ks.string(error: .text('Give it a name')).min(1),
'packed': ks.boolean().defaultTo(false),
}),
)
.min(1, error: .text('Add at least one item')),
});
import 'package:dnd_kit_flutter/dnd_kit_flutter.dart';
import 'package:flutter/material.dart';
import 'package:keyed_form_flutter/keyed_form_flutter.dart';
import 'packing_schema.dart';
part 'packing_row.dart';
class PackingList extends StatefulWidget {
const PackingList({super.key});
@override
State<PackingList> createState() => _PackingListState();
}
class _PackingListState extends State<PackingList> {
final dnd = DndController();
@override
void dispose() {
dnd.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return KeyedFieldList<PackingSchema, PackingItemSchema>(
field: PackingFields.items,
builder: (context, items, list) => Stack(
children: [
SortableScope(
controller: dnd,
strategy: SortableStrategies.dropOnOver,
offsetResolver: SortableOffsets.verticalList,
itemIds: [for (final item in items) DndId(item.clientId)],
onMove: (d) => list.move(d.fromIndex, d.toIndex),
child: Column(
children: [
for (final item in items)
SortableItem(
key: ValueKey(item.clientId),
id: DndId(item.clientId),
activationConstraint: const DndSensorActivationConstraint(
distance: 4,
),
builder: (context, drag, child) => AnimatedContainer(
duration: const Duration(milliseconds: 150),
transform: Matrix4.translationValues(0, drag.offset.y, 0),
child: Opacity(
opacity: drag.isDragging ? 0 : 1,
child: child,
),
),
child: _PackingRow(
fields: PackingFields.item(itemClientId: item.clientId),
onRemove: () => list.removeById(item.clientId),
),
),
TextButton.icon(
onPressed: () => list.append(PackingItemSchema.create()),
icon: const Icon(Icons.add),
label: const Text('Add item'),
),
],
),
),
DndDragOverlay(
controller: dnd,
builder: (context, drag) => Material(
elevation: 4,
child: ListTile(
leading: const Icon(Icons.drag_indicator),
title: Text(list.byId(drag.activeId.value)?.label ?? ''),
),
),
),
],
),
);
}
}
part of 'packing_list.dart';
class _PackingRow extends StatelessWidget {
const _PackingRow({required this.fields, required this.onRemove});
final ItemFieldRefs fields;
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
return Row(
children: [
const DndDragHandle(child: Icon(Icons.drag_indicator)),
KeyedFormField<PackingSchema, bool>(
field: fields.packed,
anchor: false,
builder: (context, f) => Checkbox(
value: f.value ?? false,
onChanged: (v) => f.onChanged(v ?? false),
),
),
Expanded(
child: KeyedFormField.text<PackingSchema>(
field: fields.label,
builder: (context, f, controller) => TextField(
controller: controller,
onTapOutside: (_) => f.onBlur(),
decoration: InputDecoration(
labelText: 'Item',
errorText: f.errorText,
),
),
),
),
IconButton(onPressed: onRemove, icon: const Icon(Icons.close)),
],
);
}
}
Edit by identity#
form.field(PackingFields.items).list() returns the list editor. It supports operations such as
append(item), insertAfter(clientId, item), move(from, to), and
removeById(clientId). Prefer by-id edits when the target row is already known; indexes are positions and change after insertion, removal, or movement. For the complete operation set, see
List operations.
Use each row's clientId to build its field reference and Flutter widget key. The row id remains stable while its index changes, keeping values, errors, and dirty tracking associated with the same record. See
Flutter lists and virtualization for lazily built rows.
Validate the collection#
List-level rules belong on the list validator (the example requires at least one row); row fields retain their own validation. Collection semantics and limits are covered in Objects and collections.