dynamic children¶
arranging children gave a container a fixed set of children, written out by hand in a Children = { ... } list. that works when you know the children at author time. when the children come from data, a list whose length and contents change at runtime, you build them in a loop with Children.AddRange.
the fixed list, and its limit¶
the collection-initializer form lists children literally:
new Container
{
Children =
{
new Text( "members" ),
new Text( "alice" ),
},
}
you cannot put a loop inside that brace list. for a data-driven list, goo gives you AddRange as an extension on a container's Children.
AddRange: one child per item¶
build the container first, then call AddRange on its Children. it takes your list and a builder function, runs the builder once per item with the index and the item, and adds each returned blob as a child. the builder can return any blob kind, not just Container: a list of Text rows or Polygon markers works the same way.
static readonly string[] Names = { "alice", "bob", "carol" };
protected override Container Build()
{
var list = new Container { FlexDirection = FlexDirection.Column, Gap = 8 };
list.Children.AddRange( Names, ( i, name ) => new Container
{
Padding = 8,
Children = { new Text( name ) },
} );
return list;
}
for a small item, an inline lambda like this is fine. when the item needs its own layout, animation, or state, move the construction into a static helper and call that from the lambda instead.
delegating to a static builder¶
the keystroke visualizer (Code/Demos/KeystrokeVisualizerUI.cs) does this to keep Build() readable. it has a column container and a queue of entries, and passes a helper delegate:
column.Children.AddRange( _queue, ( i, e ) =>
EntryChip.Build( i, e.Label, _now - e.SpawnTime, _now - e.LastInputTime, anim ) );
EntryChip.Build takes the index and the entry data and returns a fully configured Container. the caller never sees the internals of a chip. the pattern scales to any item that is complex enough to deserve its own type.
keys and identity¶
goo matches children across rebuilds by their Key. when a child has no key, identity falls back to position, which is fragile: insert or remove an item in the middle of the list and the panels below it shift identity, which can restart an animation or move focus to the wrong row.
AddRange handles the common case for you. when your builder returns a container with no Key, it assigns an index key (_idx:0, _idx:1, and so on), which is enough for a list you only ever append to or truncate.
for a list that reorders, or where items are inserted and removed in the middle, give each child a stable key drawn from the data. a key you set yourself wins over the index default, so identity follows the item rather than its slot:
list.Children.AddRange( people, ( i, p ) => new Container
{
Key = p.Id, // stable identity, survives reorder
Children = { new Text( p.Name ) },
} );
stable also means unique. draw the key from an id, never from a display value that can repeat: two rows both keyed row-ruby give the reconciler one identity for two children, so it warns and falls back to positional matching for that list, the same degraded mode as mixing keyed and unkeyed siblings. if your data has no natural id, mint one when the item enters the list (a counter field is enough) and carry it with the item.
AddIf: a child only when a condition holds¶
the sibling helper AddIf adds a single child when a condition is true and skips it otherwise:
list.Children.AddIf( _showHint, HintBanner() );
one sharp edge: the child argument is evaluated eagerly, whether or not the condition holds. HintBanner() above runs even when _showHint is false; only the add is skipped. that is fine for a cheap blob. when construction is expensive, or must not run in the false case, guard it yourself:
list.Children.AddIf( _showHint, _showHint ? HintBanner() : default );
default skips the construction entirely and is never added.
see also¶
- arranging children - the fixed children list this builds on
- container reference - the container each builder returns
- build method - when
Build()re-runs and the child list is rematched