Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
Collections

Aria provides ObservableList<T> as the mutable source and a family of derived list views that transform, filter, sort, and paginate — all reactive, all change-propagating.

Include: #include "aria/observable_list.hpp" and derived views from #include "aria/derived/filtered_list.hpp", etc.


ObservableList<T>

Thread-safe observable collection of std::shared_ptr<T>. Fires granular ListChange<T> events on every mutation.

Create and Populate

auto t1 = std::make_shared<Task>("Write docs");
tasks.append(t1);
auto t2 = std::make_shared<Task>("Ship release");
tasks.append(t2);
Observable sequence of owning element handles.
Definition observable_list.hpp:40

Mutations

// Insert at position
tasks.insert(0, std::make_shared<Task>("Urgent"));
// Remove by index
tasks.remove_at(1);
// Replace item at index
tasks.replace(0, std::make_shared<Task>("Updated"));
// Move item from index A to index B
tasks.move(2, 0);
// Clear all
tasks.clear();
void remove_at(std::size_t index)
Definition observable_list.hpp:165
void clear()
Definition observable_list.hpp:322
void move(std::size_t from, std::size_t to)
Definition observable_list.hpp:305
void insert(std::size_t index, std::shared_ptr< T > item)
Definition observable_list.hpp:126

Range Operations

std::vector<std::shared_ptr<Task>> batch = {/* ... */};
tasks.insert_range(tasks.size(), batch.begin(), batch.end());
tasks.remove_range(0, 3); // remove 3 items starting at index 0
tasks.remove_all([](const std::shared_ptr<Task>& t) {
return t->done;
});
std::size_t remove_all(Pred &&predicate)
Definition observable_list.hpp:237
std::size_t size() const
Definition observable_list.hpp:66
void insert_range(std::size_t index, InputIt first, InputIt last)
One O(n + k) insertion and k owning Insert events, in forward order.
Definition observable_list.hpp:142
void remove_range(std::size_t index, std::size_t count)
Removes in forward event order, each at the same replay pivot.
Definition observable_list.hpp:182

Syncing With a Fresh Snapshot (reconcile)

The mutators above are imperative — you name the operation. But when data arrives from a server you usually get a whole new array with no indication of what changed. reconcile works out the difference for you:

// Rows are the same logical row iff their id matches, even though the
// server hands us freshly allocated objects every refresh.
struct ById {
int operator()(const Task& t) const noexcept { return t.id; }
};
std::vector<std::shared_ptr<Task>> fresh = fetch_tasks();
tasks.reconcile(std::move(fresh), ById{});
std::size_t reconcile(std::vector< std::shared_ptr< T > > next, KeyFn key_of={})
Reconcile by unique key.
Definition observable_list.hpp:344

For unambiguous keys, this emits incremental Insert / Remove / Replace / Move events. Ambiguous duplicate keys use Reset; the algorithm does not promise the globally shortest edit stream. That distinction matters: on Reset observers must discard their mirror, so the view loses selection, scroll position, expansion state and row animations. A poll loop built on clear() + insert_range throws all of that away on every tick, even when nothing actually changed. Selection also clears itself on Reset, so a refresh would silently drop whatever the user had selected.

Pass a real key_of whenever the source allocates new objects for the same logical rows; the default identity is the object's address, which is only useful if you reuse handles. Reconciling an already-matching list emits nothing and returns 0.

Wrap the call in reactive::batch if downstream Computed values should recompute once at the end rather than per event.

Full semantics: docs/reference/list-diff-contract.md D-14.

Observe Changes

auto sub = tasks.on_change([](const aria::ListChange<Task>& change) {
switch (change.kind) {
std::cout << "Inserted at " << change.index << "\n";
break;
std::cout << "Removed at " << change.index << "\n";
break;
std::cout << "Replaced at " << change.index << "\n";
break;
std::cout << "Moved from " << change.from_index
<< " to " << change.index << "\n";
break;
std::cout << "List cleared\n";
break;
std::cout << "Item changed at " << change.index << "\n";
break;
}
});
@ Replace
Definition list_change.hpp:10
@ Remove
Definition list_change.hpp:10
@ Reset
Definition list_change.hpp:10
@ Move
Definition list_change.hpp:10
@ ItemChanged
Definition list_change.hpp:10
@ Insert
Definition list_change.hpp:10
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
std::size_t index
Definition list_change.hpp:19
std::size_t from_index
Definition list_change.hpp:21
ListChangeKind kind
Definition list_change.hpp:18

Read

std::size_t n = tasks.size();
bool empty = tasks.empty();
auto item = tasks.at(3); // shared_ptr<Task>, or nullptr if out of range
tasks.index_of(item); // O(1) amortised via internal hash map
std::shared_ptr< T > at(std::size_t index) const
Definition observable_list.hpp:68
std::size_t index_of(const T *item) const
Definition observable_list.hpp:391
bool empty() const
Definition observable_list.hpp:67

ListChangeKind Reference

Kind Index Item From Index Meaning
Insert insertion point inserted item — One item added
Remove slot before removal removed item — One item removed
Replace position new item — Item swapped
Move destination moved item source Item relocated
Reset 0 nullptr — Entire list cleared
ItemChanged current position changed item — Item's own on_changed fired

Derived Views

Derived views sit on top of an ObservableList (or another derived view) and transform the stream. They are themselves ListSource implementations — you can chain them.

FilteredList

Show only items matching a predicate:

aria::FilteredList<Task> incomplete{all_tasks,
[](const std::shared_ptr<Task>& t) { return !t->done; }
};
Definition filtered_list.hpp:100

When all_tasks mutates, incomplete recalculates and emits its own ListChange events.

SortedList

Maintain items in sorted order:

aria::SortedList<Task> sorted{all_tasks,
[](const std::shared_ptr<Task>& a, const std::shared_ptr<Task>& b) {
return a->priority > b->priority; // highest first
}
};
Definition sorted_list.hpp:96

MappedList

Transform each item into a different type:

[](const std::shared_ptr<Task>& t) -> std::shared_ptr<std::string> {
return std::make_shared<std::string>(t->title);
}
};
Definition mapped_list.hpp:77

DistinctList

Remove duplicates by key:

aria::DistinctList<Task> unique{all_tasks,
[](const std::shared_ptr<Task>& t) { return t->id; }
};
Definition distinct_list.hpp:71

PagedList

Virtualize a large list into pages:

aria::PagedList<Task> paged{all_tasks, /*page_size=*/20};
paged.page_index().set(0); // show first 20 items
paged.page_count(); // total pages
Definition paged_list.hpp:58

Re-windowing emits incremental Insert, Remove, and Move changes. Moving an existing row preserves its identity in downstream derived lists and adapters; changing the page does not require a full model reset.

GroupedList

Group items by a key function:

aria::GroupedList<Task> grouped{all_tasks,
[](const std::shared_ptr<Task>& t) { return t->category; }
};
Definition grouped_list.hpp:93

Chaining

Derived views compose — filter first, then sort:

[](const auto& t) { return !t->done; }};
[](const auto& a, const auto& b) { return a->priority > b->priority; }};
std::shared_ptr< SortedList< T, Source > > sorted(std::shared_ptr< Source > source, Comparator comparator)
Definition sorted_list.hpp:803

Mutations to source propagate through active → sorted automatically.


Quick Reference

Type Input Transform Output Events
ObservableList<T> Direct mutations — Insert/Remove/Replace/Move/Reset/ItemChanged
FilteredList<T> Any ListSource<T> Predicate filter Same kinds, subset
SortedList<T> Any ListSource<T> Comparator sort Same kinds, reordered
MappedList<U> Any ListSource<T> T → U transform Same kinds, mapped items
DistinctList<T> Any ListSource<T> Deduplicate by key Same kinds, deduplicated
PagedList<T> Any ListSource<T> Window by page Same kinds, windowed
GroupedList<T> Any ListSource<T> Group by key Group-aware events

Composing Derived Views

Every derived view is itself a valid source for another, so views chain. Use the factory helpers — they deduce the source type, which otherwise has to be written out in full (SortedList<Row, FilteredList<Row>> and worse):

auto evens = aria::filtered(tasks, [](const Task& t) { return !t.done; });
auto unique = aria::distinct<int>(evens, [](const Task& t) { return t.group_id; });
auto asc = aria::sorted(unique, [](const Task& a, const Task& b) {
return a.due < b.due;
});
auto page = aria::paged(asc, /*page_size=*/20);
std::shared_ptr< PagedList< T, Source > > paged(std::shared_ptr< Source > source, std::size_t page_size, std::size_t page_index=0)
Definition paged_list.hpp:351
std::shared_ptr< FilteredList< T, Source > > filtered(std::shared_ptr< Source > source, Predicate predicate)
Definition filtered_list.hpp:583
std::shared_ptr< DistinctList< T, Key, Source > > distinct(std::shared_ptr< Source > source, KeyFn key_of)
Definition distinct_list.hpp:612

Mutating tasks propagates all the way to page, each stage applying its own transformation. Lifetime is transitive too: each stage holds a strong reference to its source, so keeping page keeps the whole pipeline alive and the intermediate handles can go out of scope.

mapped<Target>() changes the element type mid-pipeline:

auto rows = aria::mapped<RowViewModel>(asc, [](const Task& t) {
return std::make_shared<RowViewModel>(t);
});
std::shared_ptr< MappedList< Source, Target, SourceList > > mapped(std::shared_ptr< SourceList > source, MapFn mapper, bool remap_on_change=false)
Definition mapped_list.hpp:311

Full semantics: docs/reference/list-diff-contract.md D-41.


See Also