Dynamic lists
Use stable identity rather than reconstructing all views from a getter. P.Values retains by value identity; P.Indexes can retain by explicit dictionary key. Append their result through .With(() => rows.Value). Reordering moves the retained native objects; removal disposes row bindings. Example and rules.
Update data while retaining rows
The inventory below uses item IDs 101 and 102 as stable keys. Add a potion replaces the item record with a higher quantity while preserving ID 101, so the existing row follows the new record. Reverse rows changes the order while retaining each row's native identity. Remove the key removes item 102 and disposes that row's owned bindings.
Choose keys from the identity of the item, rather than from its current position, when rows should follow items through reordering. The row receives read-only reactive item and presence values; change the source collection to update it. The example replaces arrays instead of mutating the stored array in place.
If every update rebuilds the list, check whether the children getter creates fresh declarations instead of reading the retained operator result. Keep .With(() => rows.Value) on the containing visual view, and supply unique non-null views. The retained branches API compares positional, keyed and value-based operators.
Inventory
- Potion x3
- Key x1
Try the controls. This browser demo models the example’s behavior; Unity renders the native UI. Download matching Unity source ↗
Run the example
Import both files into Assets/Examples. Keep one App.cs entry. These use the native API; the browser preview models the state changes above.
Download PineInventory.cs · Download App.cs
using System.Collections.Generic;
using System.Linq;
using Pine;
using UnityEngine;
namespace PineDocs.Examples
{
public static class PineInventory
{
private sealed class Item
{
public readonly int Id,
Quantity;
public readonly string Name;
public Item(int id, string name, int quantity)
{
Id = id;
Name = name;
Quantity = quantity;
}
}
public static View Create()
{
var items = P.Source(new[] { new Item(101, "Potion", 3), new Item(102, "Key", 1) });
var rows = P.Indexes<int, Item, View>(
() => items.Value.Select(item => new KeyValuePair<int, Item>(item.Id, item)),
(id, item, present) =>
new Branch<View>(
P.Text(
() => $"{item.Value.Name} x{item.Value.Quantity}",
name: $"Item {id}"
)
.With(P.LayoutElement(preferredHeight: 40))
)
);
return P.Vertical(
spacing: 8,
childControlWidth: true,
childControlHeight: true,
childForceExpandHeight: false,
sizeDelta: new Vector2(520, 400)
)
.With(
P.Text("Inventory").With(P.LayoutElement(preferredHeight: 40)),
P.Vertical(
childControlWidth: true,
childControlHeight: true,
childForceExpandHeight: false
)
.With(P.LayoutElement(preferredHeight: 120))
.With(() => rows.Value),
P.Button(
"Add a potion",
onClick: () =>
items.Value = items
.Value.Select(item =>
item.Id == 101
? new Item(item.Id, item.Name, item.Quantity + 1)
: item
)
.ToArray()
)
.With(P.LayoutElement(preferredHeight: 40)),
P.Button(
"Reverse rows",
onClick: () => items.Value = items.Value.Reverse().ToArray()
)
.With(P.LayoutElement(preferredHeight: 40)),
P.Button(
"Remove the key",
interactable: new Value<bool>(() =>
items.Value.Any(item => item.Id == 102)
),
onClick: () =>
items.Value = items.Value.Where(item => item.Id != 102).ToArray()
)
.With(P.LayoutElement(preferredHeight: 40))
);
}
}
}
