I followed the documentation on how to emit an event from native side to react-native.
I have a RecyclerViewScrollListener
, which is defined like this.
Basically, I want to send the current item's index to React-Native, like the FlatList's onViewableItemsChanged
event.
scrollListener = object : RecyclerViewScrollListener() {
override fun onItemIsFirstVisibleItem(index: Int) {
Log.d("visible item index", index.toString())
// play just visible item
if (index != -1) {
PlayerViewAdapter.playIndexThenPausePreviousPlayer(index)
eventEmitter.emitEvent("onVisibleItemChanged", index)
}
}
}
recyclerView!!.addOnScrollListener(scrollListener)
It is defined in TikTokScreenFragment's onViewCreated
override
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setAdapter() // this is where the scroll listener is set up
// load data
val model: MediaViewModel by activityViewModels()
model.getMedia().observe(requireActivity(), Observer {
mAdapter?.updateList(arrayListOf(*it.toTypedArray()))
})
}
The eventEmitter.emitEvent
implementation is as below inside a ViewManager that later will be listed in my custom ReactPackage
:
class MyViewManager(
private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>() {
override fun receiveCommand(
root: FrameLayout,
commandId: String,
args: ReadableArray?
) {
super.receiveCommand(root, commandId, args)
val reactNativeViewId = requireNotNull(args).getInt(0)
when (commandId.toInt()) {
COMMAND_CREATE -> createFragment(root, reactNativeViewId)
}
}
fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
setupLayout(parentView)
val eventEmitter = object : EventEmitter {
override fun emitEvent(eventName: String) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, null)
}
override fun emitEvent(eventName: String, eventData: Int) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, eventData)
}
}
val myFragment = TikTokScreenFragment(eventEmitter)
val activity = reactContext.currentActivity as FragmentActivity
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, myFragment, reactNativeViewId.toString())
.commit()
}
}
On the react side, I use NativeEventEmitter
to subscribe for the changes in a useEffect and set the returned Index to a state:
useEffect(() => {
const eventEmitter = new NativeEventEmitter();
const onVisibleItemChanged = eventEmitter.addListener(
"onVisibleItemChanged",
function (e: Event) {
// handle event.
setVisibleIndex(e as unknown as number);
}
);
console.log("listening to onVisibleItemChanged");
return () => {
console.log("removing onVisibleItemChanged listener");
onVisibleItemChanged.remove();
};
}, []);
I observed the memory in the Android memory profiler sky-rocketed while scrolling through the screen. If I commented out the eventEmitter.addListener
, the problem disappeared.
Any suggestions or thoughts about what I can try are welcome. I'm a newbie in Android development, so I'm probably doing something wrong along the way.
My bad on this. It wasn't a memory leak on the native side but is a problem on React. I use react-query in my app, and a useQuery
hook is inside each Recycler item. Every time users scroll past the screen, the useQuery's value remains in memory because I believe the gcTime
option is 5 minutes, which adds up, causing a lot of memory consumption for already stale values.