Skip to content

Commit 7cdd5f7

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 5e01765 commit 7cdd5f7

21 files changed

Lines changed: 608 additions & 31 deletions

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
@@ -11,11 +11,16 @@ import {
1111
import { NavigationContainer, useNavigation } from '@react-navigation/native';
1212
import { createStackNavigator } from '@react-navigation/stack';
1313
import AsyncStorage from '@react-native-async-storage/async-storage';
14+
import { runOnUI } from 'react-native-reanimated';
15+
import { installWorkletDispatcher } from '@rive-app/react-native';
1416
import { PagesList, type PageItem } from './PagesList';
1517
import { HomeMenu } from './shared/HomeMenu';
1618

1719
const LAST_OPENED_KEY = '@rive_example_last_opened';
1820

21+
// Install dispatcher on Reanimated's UI runtime for worklet-based listeners
22+
installWorkletDispatcher(runOnUI);
23+
1924
type RootStackParamList = {
2025
Home: undefined;
2126
} & {

0 commit comments

Comments
 (0)