Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
JniRecyclerNotifier.hpp
Go to the documentation of this file.
1#pragma once
2
3// JniRecyclerNotifier.hpp — the JNI half of the RecyclerView list bridge.
4//
5// `JniListSource<T>` (JniListSource.hpp) owns the snapshot and the
6// diffing and deliberately has no `<jni.h>` dependency so its logic is
7// host-testable. This header supplies the piece that genuinely needs a
8// JVM: a `NotifySink` that calls `notifyItemInserted` /
9// `notifyItemRemoved` / `notifyItemChanged` / `notifyItemMoved` /
10// `notifyDataSetChanged` on a managed `RecyclerView.Adapter`.
11//
12// Usage (C++ side, typically from a JNI entry point):
13//
14// // `adapter` is the Kotlin RecyclerView.Adapter instance.
15// auto notifier = std::make_shared<JniRecyclerNotifier>(env, adapter);
16// auto rows = std::make_unique<JniListSource<Movie>>(
17// vm.movies, notifier->sink());
18//
19// Then forward the managed adapter's overrides to the C++ side:
20//
21// getItemCount() -> rows->item_count()
22// onBindViewHolder(holder, pos) -> rows->at(pos)
23//
24// Threading
25// ---------
26// RecyclerView notifications MUST be raised on the Android main thread.
27// This notifier calls straight through. For off-main list mutations,
28// give JniListSource an owner-thread dispatcher so snapshot updates and
29// notifications both run on the main looper (see JniListSource.hpp).
30//
31// The notifier attaches a JNIEnv for the calling thread when needed, so
32// a sink invoked from a non-JNI thread does not crash — it will still be
33// the caller's bug if that thread is not the main looper, but it will
34// surface as an Android RecyclerView complaint rather than as JNI UB.
35
37#include "aria/adapters/jni/detail/jni_support.hpp"
38
39#include <jni.h>
40
41#include <atomic>
42#include <functional>
43#include <limits>
44#include <memory>
45
46namespace aria::adapters::jni {
47
56public:
57 JniRecyclerNotifier(JNIEnv* env, jobject adapter) : state_(std::make_shared<State>()) {
58 if (!env || !adapter) return;
59 auto& state = *state_;
60 state.vm = detail::vm_of(env);
61 if (!state.vm) return;
62 state.adapter = env->NewGlobalRef(adapter);
63 if (detail::check_exception(env, "NewGlobalRef") || !state.adapter) return;
64 state.inserted = detail::method(env, state.adapter, "notifyItemInserted", "(I)V");
65 if (!state.inserted) return;
66 state.removed = detail::method(env, state.adapter, "notifyItemRemoved", "(I)V");
67 if (!state.removed) return;
68 state.changed = detail::method(env, state.adapter, "notifyItemChanged", "(I)V");
69 if (!state.changed) return;
70 state.moved = detail::method(env, state.adapter, "notifyItemMoved", "(II)V");
71 if (!state.moved) return;
72 state.dataset = detail::method(env, state.adapter, "notifyDataSetChanged", "()V");
73 state.active.store(state.dataset != nullptr, std::memory_order_release);
74 }
75
76 ~JniRecyclerNotifier() { state_->active.store(false, std::memory_order_release); }
79
80 [[nodiscard]] bool valid() const noexcept {
81 return state_->active.load(std::memory_order_acquire);
82 }
83
86 [[nodiscard]] std::function<void(const RecyclerNotification&)> sink() {
87 return [weak = std::weak_ptr{state_}](const RecyclerNotification& notification) {
88 if (auto state = weak.lock()) dispatch_(state, notification);
89 };
90 }
91
92 void dispatch(const RecyclerNotification& notification) { dispatch_(state_, notification); }
93
94private:
95 struct State {
96 JavaVM* vm = nullptr;
97 jobject adapter = nullptr;
98 jmethodID inserted = nullptr, removed = nullptr, changed = nullptr;
99 jmethodID moved = nullptr, dataset = nullptr;
100 std::atomic<bool> active{false};
101 ~State() { detail::delete_global_ref(vm, adapter); }
102 };
103
104 static void dispatch_(std::shared_ptr<State> state, const RecyclerNotification& n) {
105 if (!state->active.load(std::memory_order_acquire)) return;
106 constexpr auto maximum = static_cast<std::size_t>(std::numeric_limits<jint>::max());
108 && (n.position > maximum || (n.kind == RecyclerNotify::ItemMoved && n.from_position > maximum))) {
109 ::aria::report_callback_failure("jni.recycler", "notification position exceeds jint range");
110 return;
111 }
112 detail::Env scope(state->vm);
113 auto* env = scope.get();
114 if (!env || detail::check_exception(env, "RecyclerView notification")) return;
115 if (!state->active.load(std::memory_order_acquire)) return;
116 switch (n.kind) {
118 env->CallVoidMethod(state->adapter, state->inserted, static_cast<jint>(n.position)); break;
120 env->CallVoidMethod(state->adapter, state->removed, static_cast<jint>(n.position)); break;
122 env->CallVoidMethod(state->adapter, state->changed, static_cast<jint>(n.position)); break;
124 env->CallVoidMethod(state->adapter, state->moved, static_cast<jint>(n.from_position), static_cast<jint>(n.position)); break;
126 env->CallVoidMethod(state->adapter, state->dataset); break;
127 }
128 detail::check_exception(env, "RecyclerView notification");
129 }
130
131 std::shared_ptr<State> state_;
132};
133
134} // namespace aria::adapters::jni
JniRecyclerNotifier & operator=(const JniRecyclerNotifier &)=delete
bool valid() const noexcept
Definition JniRecyclerNotifier.hpp:80
std::function< void(const RecyclerNotification &)> sink()
A retained sink becomes inert at notifier destruction.
Definition JniRecyclerNotifier.hpp:86
JniRecyclerNotifier(JNIEnv *env, jobject adapter)
Definition JniRecyclerNotifier.hpp:57
void dispatch(const RecyclerNotification &notification)
Definition JniRecyclerNotifier.hpp:92
JniRecyclerNotifier(const JniRecyclerNotifier &)=delete
~JniRecyclerNotifier()
Definition JniRecyclerNotifier.hpp:76
JniAdapter — Android JNI implementation of aria::binding::IViewAdapter.
Definition JniAdapter.hpp:54
@ 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.
Definition validation_key.hpp:110
position is the changed row (the destination for a move); from_position is meaningful only for ItemMo...
Definition JniListSource.hpp:36