Start with a typed row builder.
Use a fixed item height for uniform rows. Omitting it selects native dynamic-height virtualization.
new ListView<Project>(
projects,
project => new ProjectRow(project),
itemHeight: 56f);Key rows that carry state.
Give every row a deterministic, unique key when it owns focus, subscriptions or local state. A key follows a compatible realized row through insertion and reordering.
new ListView<Project>(
projects,
project => new ProjectRow(project),
itemHeight: 56f,
itemKey: project => new WidgetKey(project.Id));For reactive replacement, pass State<IReadOnlyList<T>> and replace its complete value. A mutable collection cannot notify the list by itself.
Keep selection outside the list.
Selection is controlled and key-based. User interaction commits the state before the callback runs, while programmatic changes update native selection without replaying the callback.
var selected = new State<IReadOnlyList<WidgetKey>>(
Array.Empty<WidgetKey>());
new ListView<Project>(
projects, BuildRow,
itemKey: project => new WidgetKey(project.Id),
selectionMode: ListSelectionMode.Multiple,
selectedKeys: selected);Retain a controller when scroll position matters.
Keep the controller at the same ownership boundary as the list configuration. It restores the offset after the native list has its geometry.
private readonly ListViewController _projectsController = new();
_projectsController.JumpTo(320f);
_projectsController.ScrollTo(new WidgetKey(projectId));