Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
JniListSource.hpp
Go to the documentation of this file.
1#pragma once
2
3// RecyclerView mirror for any ListSourceOf<L, T>. Snapshot changes and native
4// notifications are delivered together, in source-event order. When producers
5// run off the Android main thread, supply a dispatcher backed by that looper;
6// posting only the notification would expose the wrong mirror to RecyclerView.
7// This header has no JNI dependency and can be tested on a desktop host.
8
10#include "aria/list_source.hpp"
12#include "aria/subscription.hpp"
13
14#include <cstddef>
15#include <deque>
16#include <functional>
17#include <memory>
18#include <mutex>
19#include <optional>
20#include <stdexcept>
21#include <utility>
22#include <vector>
23
24namespace aria::adapters::jni {
25
33
41
42template<typename T>
44public:
45 using NotifySink = std::function<void(const RecyclerNotification&)>;
46
56 template<class L>
57 requires ::aria::ListSourceOf<L, T>
58 JniListSource(L& source, NotifySink sink,
59 std::shared_ptr<runtime::IDispatcher> dispatcher = {})
60 : state_(std::make_shared<State>(source.snapshot(), std::move(sink),
61 std::move(dispatcher))) {
62 sub_ = source.observe([weak = std::weak_ptr<State>(state_)](const ListChange<T>& change) {
63 if (auto state = weak.lock()) enqueue_(state, change);
64 });
65 }
66
68 close_(state_);
69 sub_.release();
70 }
71 JniListSource(const JniListSource&) = delete;
73
74 [[nodiscard]] std::size_t item_count() const {
75 std::lock_guard lock(state_->mutex);
76 return state_->rows.size();
77 }
78
81 [[nodiscard]] std::shared_ptr<T> at(std::size_t position) const {
82 std::lock_guard lock(state_->mutex);
83 return position < state_->rows.size() ? state_->rows[position] : nullptr;
84 }
85
89 void reload() {
90 auto state = state_;
91 enqueue_(state, std::nullopt);
92 }
93
94private:
95 using Rows = std::vector<std::shared_ptr<T>>;
96 // nullopt is a reload request, ordered with normal changes in the queue.
97 using Event = std::optional<ListChange<T>>;
98 struct State {
99 State(Rows initial, NotifySink callback, std::shared_ptr<runtime::IDispatcher> owner)
100 : rows(std::move(initial)), sink(std::make_shared<NotifySink>(std::move(callback))),
101 dispatcher(std::move(owner)) {}
102 std::mutex mutex;
103 bool active = true;
104 bool scheduled = false;
105 Rows rows;
106 std::shared_ptr<NotifySink> sink;
107 std::shared_ptr<runtime::IDispatcher> dispatcher;
108 std::deque<Event> pending;
109 };
110
111 static void close_(const std::shared_ptr<State>& state) noexcept {
112 Rows retired_rows;
113 std::shared_ptr<NotifySink> retired_sink;
114 std::deque<Event> retired_events;
115 {
116 std::lock_guard lock(state->mutex);
117 state->active = false;
118 retired_rows.swap(state->rows);
119 retired_sink.swap(state->sink);
120 retired_events.swap(state->pending);
121 }
122 // Items and callback captures may re-enter the bridge on destruction.
123 }
124
125 static void enqueue_(const std::shared_ptr<State>& state, Event event) {
126 std::shared_ptr<runtime::IDispatcher> dispatcher;
127 {
128 std::lock_guard lock(state->mutex);
129 if (!state->active) return;
130 state->pending.push_back(std::move(event));
131 if (state->scheduled) return;
132 state->scheduled = true;
133 dispatcher = state->dispatcher;
134 }
135 if (dispatcher && !dispatcher->is_main_thread()) {
136 try {
137 dispatcher->post([weak = std::weak_ptr<State>(state)] {
138 if (auto owner = weak.lock()) drain_(owner);
139 });
140 } catch (...) {
141 std::deque<Event> retired;
142 {
143 std::lock_guard lock(state->mutex);
144 state->scheduled = false;
145 retired.swap(state->pending);
146 }
147 report_callback_failure("jni.list_source.dispatch", std::current_exception());
148 }
149 } else {
150 drain_(state);
151 }
152 }
153
154 static void drain_(const std::shared_ptr<State>& state) noexcept {
155 while (true) {
156 Event event;
157 {
158 std::lock_guard lock(state->mutex);
159 if (!state->active || state->pending.empty()) {
160 state->scheduled = false;
161 return;
162 }
163 event = std::move(state->pending.front());
164 state->pending.pop_front();
165 }
166 try {
167 apply_(state, event);
168 } catch (...) {
169 report_callback_failure("jni.list_source.change", std::current_exception());
170 }
171 }
172 }
173
174 static void apply_(const std::shared_ptr<State>& state, const Event& event) {
175 using K = ListChangeKind;
176 Rows reset_rows;
177 if (event && event->kind == K::Reset) {
178 if (!event->snapshot) throw std::invalid_argument("JniListSource: Reset needs a snapshot");
179 reset_rows = *event->snapshot;
180 }
181 std::shared_ptr<T> retired_item;
182 std::shared_ptr<NotifySink> sink;
183 RecyclerNotification notification;
184 {
185 std::lock_guard lock(state->mutex);
186 if (!state->active) return;
187 auto& rows = state->rows;
188 if (event) {
189 const auto& change = *event;
190 const auto index = change.index;
191 switch (change.kind) {
192 case K::Insert:
193 if (index > rows.size()) throw std::out_of_range("JniListSource: insert index");
194 rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(index), change.item);
195 notification = {RecyclerNotify::ItemInserted, index, 0};
196 break;
197 case K::Remove:
198 if (index >= rows.size()) return;
199 retired_item = std::move(rows[index]);
200 rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(index));
201 notification = {RecyclerNotify::ItemRemoved, index, 0};
202 break;
203 case K::Replace:
204 case K::ItemChanged:
205 if (index >= rows.size()) return;
206 retired_item = std::move(rows[index]);
207 rows[index] = change.item;
208 notification = {RecyclerNotify::ItemChanged, index, 0};
209 break;
210 case K::Move: {
211 const auto from = change.from_index;
212 if (from >= rows.size() || index >= rows.size() || from == index) return;
213 auto moved = std::move(rows[from]);
214 rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(from));
215 rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(index), std::move(moved));
216 notification = {RecyclerNotify::ItemMoved, index, from};
217 break;
218 }
219 case K::Reset:
220 rows.swap(reset_rows);
221 break;
222 }
223 }
224 sink = state->sink;
225 }
226 // Keep the sink alive independently: it may destroy the bridge while
227 // running. Retired row objects are also released outside the mutex.
228 if (sink && *sink) (*sink)(notification);
229 }
230
231 std::shared_ptr<State> state_;
232 Subscription sub_;
233};
234
235} // namespace aria::adapters::jni
JniListSource & operator=(const JniListSource &)=delete
std::function< void(const RecyclerNotification &)> NotifySink
Definition JniListSource.hpp:45
JniListSource(const JniListSource &)=delete
std::size_t item_count() const
Definition JniListSource.hpp:74
std::shared_ptr< T > at(std::size_t position) const
Out-of-range reads return nullptr.
Definition JniListSource.hpp:81
~JniListSource()
Definition JniListSource.hpp:67
requires ::aria::ListSourceOf< L, T > JniListSource(L &source, NotifySink sink, std::shared_ptr< runtime::IDispatcher > dispatcher={})
Construct on the source's graph thread.
Definition JniListSource.hpp:58
void reload()
Ask a reattached RecyclerView to redisplay the current event mirror.
Definition JniListSource.hpp:89
JniAdapter — Android JNI implementation of aria::binding::IViewAdapter.
Definition JniAdapter.hpp:54
RecyclerNotify
Definition JniListSource.hpp:26
@ ItemRemoved
Definition JniListSource.hpp:28
@ DataSetChanged
Definition JniListSource.hpp:31
@ ItemMoved
Definition JniListSource.hpp:30
@ ItemChanged
Definition JniListSource.hpp:29
@ ItemInserted
Definition JniListSource.hpp:27
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
ListChangeKind
Definition list_change.hpp:10
Definition validation_key.hpp:110
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
std::size_t index
Definition list_change.hpp:19
std::shared_ptr< T > item
Definition list_change.hpp:20
std::size_t from_index
Definition list_change.hpp:21
ListChangeKind kind
Definition list_change.hpp:18
position is the changed row (the destination for a move); from_position is meaningful only for ItemMo...
Definition JniListSource.hpp:36
std::size_t position
Definition JniListSource.hpp:38
RecyclerNotify kind
Definition JniListSource.hpp:37
std::size_t from_position
Definition JniListSource.hpp:39