Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — 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:99

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:91

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:76

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:618

Mutations to source propagate through activesorted 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:588
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:299

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


See Also