Search code examples
androidreact-nativekotlinmemory-leaks

Sending event from Android to JavaScript caused memory leak despite unsubscribed in useEffect


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.


Solution

  • 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.