Skip to content

Commit 22193fc

Browse files
committed
feat: add UI thread listener support for Reanimated shared values
Add RiveWorkletBridge HybridObject that installs Nitro's dispatcher on Reanimated's UI runtime, enabling ViewModel property listeners to run on the UI thread and update SharedValues without blocking when JS is busy. - iOS: Uses GCD dispatch_async/dispatch_sync to main queue - Android: Uses Handler(Looper.getMainLooper()) via JNI bridge - Export installWorkletDispatcher() function from package - Update example to demonstrate JS thread blocking test
1 parent 3bfa14a commit 22193fc

22 files changed

+693
-52
lines changed

RNRive.podspec

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,20 @@ Pod::Spec.new do |s|
4141
s.platforms = { :ios => min_ios_version_supported }
4242
s.source = { :git => "https://github.com/rive-app/rive-nitro-react-native.git", :tag => "#{s.version}" }
4343

44-
s.source_files = "ios/**/*.{h,m,mm,swift}"
44+
s.source_files = "ios/**/*.{h,m,mm,swift}", "cpp/**/*.{hpp,cpp}"
4545

4646
s.public_header_files = ['ios/RCTSwiftLog.h']
47+
s.private_header_files = ['cpp/**/*.hpp']
48+
49+
# Set pod_target_xcconfig BEFORE add_nitrogen_files so it gets merged with Nitro's settings
50+
s.pod_target_xcconfig = {
51+
'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"'
52+
}
53+
4754
load 'nitrogen/generated/ios/RNRive+autolinking.rb'
4855
add_nitrogen_files(s)
4956

5057
s.dependency "RiveRuntime", rive_ios_version
5158

52-
install_modules_dependencies(s)
59+
install_modules_dependencies(s)
5360
end

android/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ set(CMAKE_VERBOSE_MAKEFILE ON)
66
set(CMAKE_CXX_STANDARD 20)
77

88
# Define C++ library and add all sources
9-
add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
9+
add_library(${PACKAGE_NAME} SHARED
10+
src/main/cpp/cpp-adapter.cpp
11+
src/main/cpp/JRiveWorkletDispatcher.cpp
12+
)
1013

1114
# Add Nitrogen specs :)
1215
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/rive+autolinking.cmake)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#include "JRiveWorkletDispatcher.hpp"
2+
#include <android/log.h>
3+
4+
namespace margelo::nitro::rive {
5+
6+
using namespace facebook;
7+
8+
JRiveWorkletDispatcher::JRiveWorkletDispatcher(
9+
jni::alias_ref<JRiveWorkletDispatcher::jhybridobject> jThis)
10+
: _javaPart(jni::make_global(jThis)) {}
11+
12+
jni::local_ref<JRiveWorkletDispatcher::jhybriddata> JRiveWorkletDispatcher::initHybrid(
13+
jni::alias_ref<jhybridobject> jThis) {
14+
return makeCxxInstance(jThis);
15+
}
16+
17+
jni::local_ref<JRiveWorkletDispatcher::javaobject> JRiveWorkletDispatcher::create() {
18+
return newObjectJavaArgs();
19+
}
20+
21+
void JRiveWorkletDispatcher::trigger() {
22+
std::unique_lock lock(_mutex);
23+
while (!_jobs.empty()) {
24+
auto job = std::move(_jobs.front());
25+
_jobs.pop();
26+
lock.unlock();
27+
job();
28+
lock.lock();
29+
}
30+
}
31+
32+
void JRiveWorkletDispatcher::scheduleTrigger() {
33+
static const auto method = _javaPart->getClass()->getMethod<void()>("scheduleTrigger");
34+
method(_javaPart.get());
35+
}
36+
37+
void JRiveWorkletDispatcher::runAsync(std::function<void()>&& function) {
38+
std::unique_lock lock(_mutex);
39+
_jobs.push(std::move(function));
40+
lock.unlock();
41+
scheduleTrigger();
42+
}
43+
44+
void JRiveWorkletDispatcher::runSync(std::function<void()>&& function) {
45+
std::mutex mtx;
46+
std::condition_variable cv;
47+
bool done = false;
48+
49+
runAsync([&]() {
50+
function();
51+
{
52+
std::lock_guard<std::mutex> lock(mtx);
53+
done = true;
54+
}
55+
cv.notify_one();
56+
});
57+
58+
std::unique_lock<std::mutex> lock(mtx);
59+
cv.wait(lock, [&]{ return done; });
60+
}
61+
62+
void JRiveWorkletDispatcher::registerNatives() {
63+
registerHybrid({
64+
makeNativeMethod("initHybrid", JRiveWorkletDispatcher::initHybrid),
65+
makeNativeMethod("trigger", JRiveWorkletDispatcher::trigger),
66+
});
67+
}
68+
69+
AndroidMainThreadDispatcher::AndroidMainThreadDispatcher(
70+
jni::local_ref<JRiveWorkletDispatcher::javaobject> javaDispatcher)
71+
: _javaDispatcher(jni::make_global(javaDispatcher)) {}
72+
73+
void AndroidMainThreadDispatcher::runAsync(std::function<void()>&& function) {
74+
_javaDispatcher->cthis()->runAsync(std::move(function));
75+
}
76+
77+
void AndroidMainThreadDispatcher::runSync(std::function<void()>&& function) {
78+
_javaDispatcher->cthis()->runSync(std::move(function));
79+
}
80+
81+
} // namespace margelo::nitro::rive
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#pragma once
2+
3+
#include <fbjni/fbjni.h>
4+
#include <NitroModules/Dispatcher.hpp>
5+
#include <queue>
6+
#include <mutex>
7+
#include <condition_variable>
8+
9+
namespace margelo::nitro::rive {
10+
11+
using namespace facebook;
12+
13+
class JRiveWorkletDispatcher : public jni::HybridClass<JRiveWorkletDispatcher> {
14+
public:
15+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/RiveWorkletDispatcher;";
16+
17+
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject> jThis);
18+
static void registerNatives();
19+
20+
static jni::local_ref<javaobject> create();
21+
22+
void runAsync(std::function<void()>&& function);
23+
void runSync(std::function<void()>&& function);
24+
25+
private:
26+
friend HybridBase;
27+
28+
void trigger();
29+
void scheduleTrigger();
30+
31+
jni::global_ref<JRiveWorkletDispatcher::javaobject> _javaPart;
32+
std::queue<std::function<void()>> _jobs;
33+
std::recursive_mutex _mutex;
34+
35+
explicit JRiveWorkletDispatcher(jni::alias_ref<JRiveWorkletDispatcher::jhybridobject> jThis);
36+
};
37+
38+
class AndroidMainThreadDispatcher : public Dispatcher {
39+
public:
40+
explicit AndroidMainThreadDispatcher(jni::local_ref<JRiveWorkletDispatcher::javaobject> javaDispatcher);
41+
42+
void runAsync(std::function<void()>&& function) override;
43+
void runSync(std::function<void()>&& function) override;
44+
45+
private:
46+
jni::global_ref<JRiveWorkletDispatcher::javaobject> _javaDispatcher;
47+
};
48+
49+
} // namespace margelo::nitro::rive
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#include <jni.h>
22
#include "riveOnLoad.hpp"
3+
#include "JRiveWorkletDispatcher.hpp"
34

45
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5-
return margelo::nitro::rive::initialize(vm);
6+
auto result = margelo::nitro::rive::initialize(vm);
7+
margelo::nitro::rive::JRiveWorkletDispatcher::registerNatives();
8+
return result;
69
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.margelo.nitro.rive
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import androidx.annotation.Keep
6+
import com.facebook.jni.HybridData
7+
import com.facebook.proguard.annotations.DoNotStrip
8+
import java.util.concurrent.atomic.AtomicBoolean
9+
10+
@Suppress("JavaJniMissingFunction")
11+
@Keep
12+
@DoNotStrip
13+
class RiveWorkletDispatcher {
14+
@DoNotStrip
15+
@Suppress("unused")
16+
private val mHybridData: HybridData = initHybrid()
17+
18+
private val mainHandler = Handler(Looper.getMainLooper())
19+
private val active = AtomicBoolean(true)
20+
21+
private val triggerRunnable = Runnable {
22+
synchronized(active) {
23+
if (active.get()) {
24+
trigger()
25+
}
26+
}
27+
}
28+
29+
private external fun initHybrid(): HybridData
30+
private external fun trigger()
31+
32+
@DoNotStrip
33+
@Suppress("unused")
34+
private fun scheduleTrigger() {
35+
mainHandler.post(triggerRunnable)
36+
}
37+
38+
fun deactivate() {
39+
synchronized(active) {
40+
active.set(false)
41+
}
42+
}
43+
44+
companion object {
45+
init {
46+
System.loadLibrary("rive")
47+
}
48+
}
49+
}

cpp/HybridRiveWorkletBridge.hpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#pragma once
2+
3+
#include "HybridRiveWorkletBridgeSpec.hpp"
4+
#include <NitroModules/Dispatcher.hpp>
5+
6+
#if __APPLE__
7+
#include <dispatch/dispatch.h>
8+
#include <pthread.h>
9+
#elif __ANDROID__
10+
#include "JRiveWorkletDispatcher.hpp"
11+
#endif
12+
13+
namespace margelo::nitro::rive {
14+
15+
#if __APPLE__
16+
17+
/**
18+
* iOS: A dispatcher that runs work on the main thread using GCD.
19+
*/
20+
class MainThreadDispatcher : public Dispatcher {
21+
public:
22+
void runAsync(std::function<void()>&& function) override {
23+
__block auto func = std::move(function);
24+
dispatch_async(dispatch_get_main_queue(), ^{
25+
func();
26+
});
27+
}
28+
29+
void runSync(std::function<void()>&& function) override {
30+
if (pthread_main_np() != 0) {
31+
function();
32+
} else {
33+
__block auto func = std::move(function);
34+
dispatch_sync(dispatch_get_main_queue(), ^{
35+
func();
36+
});
37+
}
38+
}
39+
};
40+
41+
#endif
42+
43+
class HybridRiveWorkletBridge : public HybridRiveWorkletBridgeSpec {
44+
public:
45+
HybridRiveWorkletBridge() : HybridObject(TAG) {}
46+
47+
void install() override {
48+
throw std::runtime_error("install() requires runtime access - use raw method");
49+
}
50+
51+
protected:
52+
void loadHybridMethods() override {
53+
HybridObject::loadHybridMethods();
54+
registerHybrids(this, [](Prototype& prototype) {
55+
prototype.registerRawHybridMethod("install", 0, &HybridRiveWorkletBridge::installRaw);
56+
});
57+
}
58+
59+
private:
60+
jsi::Value installRaw(jsi::Runtime& runtime,
61+
const jsi::Value& thisValue,
62+
const jsi::Value* args,
63+
size_t count) {
64+
#if __APPLE__
65+
auto dispatcher = std::make_shared<MainThreadDispatcher>();
66+
Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher);
67+
#elif __ANDROID__
68+
// Create the Java dispatcher instance and wrap it in the C++ dispatcher
69+
auto javaDispatcher = JRiveWorkletDispatcher::create();
70+
auto dispatcher = std::make_shared<AndroidMainThreadDispatcher>(javaDispatcher);
71+
Dispatcher::installRuntimeGlobalDispatcher(runtime, dispatcher);
72+
#endif
73+
return jsi::Value::undefined();
74+
}
75+
};
76+
77+
} // namespace margelo::nitro::rive

example/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import {
77
} from 'react-native';
88
import { NavigationContainer } from '@react-navigation/native';
99
import { createStackNavigator } from '@react-navigation/stack';
10+
import { runOnUI } from 'react-native-reanimated';
11+
import { installWorkletDispatcher } from '@rive-app/react-native';
1012
import { PagesList } from './PagesList';
1113

14+
// Install dispatcher on Reanimated's UI runtime for worklet-based listeners
15+
installWorkletDispatcher(runOnUI);
16+
1217
type RootStackParamList = {
1318
Home: undefined;
1419
} & {

0 commit comments

Comments
 (0)