React Native allows developers to build and call native iOS and Android modules from the frontend javascript code. The React Native Android native module guide shows how a developer can quickly implement a native Android module for React Native. However, the guide does not explicitly show how to prevent the native module from blocking the main thread utilized by the UI. As a result of the native module blocking the main thread, the UI does not render as expected. The UI calls the native module, which blocks the main thread and any potential rendering that should happen while the native module is running.

For example, if a developer wants to add a spinning wheel to indicate something is happening while the native module runs, the code might look like:

App.js

const App = () => {
  const [loading, setLoading] = useState(false);

  const onPress = () => {
    setLoading(true);
    console.log('We will invoke the native module here!');
    MyModule.longMethod('3000')
      .then(response => console.log('After long method:', response))
      .then(() => setLoading(false));
  };

  return (
    <View>
      <Text>My App</Text>
      <Button title="Click to invoke your native module!" onPress={onPress} />
      {loading && (
        <View>
          <ActivityIndicator size="large" color="black" />
        </View>
      )}
    </View>
  );
};

MyModule.java

public class MyModule extends ReactContextBaseJavaModule {
    MyModule(ReactApplicationContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "MyModule";
    }

    @ReactMethod
    public void longMethod(String timeS, Promise promise) {
        int time = Integer.ParseInt(timeS);
        try {
            Thread.sleep(time);
            promise.resolve("Finished sleeping");
        } catch (InterruptedException e) {
            promise.reject("Exception while sleeping");
        }
    }
}

In this example, the developer would expect the spinner to appear for 3 seconds and then disappear. However, the native module hijacks the main thread and prevents the UI from rendering the spinner. When the native module is done running, then the UI renders the spinner for an instant before the loading variable is set back to false and the spinner disappears.

The reason for this behavior is explained at the end of the Android native module guide:

Threading

To date, on Android, all native module async methods execute on one thread. Native modules should not have any assumptions about what thread they are being called on, as the current assignment is subject to change in the future. If a blocking call is required, the heavy work should be dispatched to an internally managed worker thread, and any callbacks distributed from there.

To prevent the native module from blocking the main UI thread, the developer must wrap the native module method in a Runnable class and execute it using the ExecutorService:

MyModule.java

// Add import statement
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

...

public class MyModule extends ReactContextBaseJavaModule {
    MyModule(ReactApplicationContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "MyModule";
    }

    @ReactMethod
    public void longMethod(String timeS, Promise promise) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Runnable task = new MyThread(timeS, promise);

        executor.execute(task);
    }

    private class MyThread implements Runnable {
        private Promise promise;
        private int time;

        MyThread(String timeS, Promise newpromise) {
            this.time = Integer.parseInt(timeS);
            this.promise = newpromise;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(this.time);
                this.promise.resolve("Finished sleeping");
            } catch(InterruptedException e) {
                this.promise.reject("Exception while sleeping");
            }
        }
    }
}

This produces the expected behavior of the spinner appearing for 3 seconds and then disappearing once the native method is done. The example used Thread.sleep to mimic a long operation, but it can be replaced with any code:

MyModule.java

...

        @Override
        public void run() {
            // Add code here
        }

...

Here are the complete App.js and MyModule.java files:

App.js

import React, {useState} from 'react';
import {
  ActivityIndicator,
  Button,
  NativeModules,
  StyleSheet,
  Text,
  View,
} from 'react-native';

const {MyModule} = NativeModules;

const App = () => {
  const [loading, setLoading] = useState(false);

  const onPress = () => {
    setLoading(true);
    console.log('We will invoke the native module here!');
    MyModule.longMethod('3000')
      .then(response => console.log('After long method:', response))
      .then(() => setLoading(false));
  };

  return (
    <View style={styles.app}>
      <Text>My App</Text>
      <Button title="Click to invoke your native module!" onPress={onPress} />
      {loading && (
        <View style={styles.loading}>
          <ActivityIndicator size="large" color="black" />
        </View>
      )}
    </View>
  );
};

export default App;

const styles = StyleSheet.create({
  app: {
    height: '100%',
  },
  loading: {
    position: 'absolute',
    left: 0,
    right: 0,
    top: 0,
    bottom: 0,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#000000',
    opacity: 0.7,
  },
});

MyModule.java

package com.androidnativemoduleexample; // replace com.androidnativemoduleexample with your app’s name
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.Map;
import java.util.HashMap;

public class MyModule extends ReactContextBaseJavaModule {
    MyModule(ReactApplicationContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "MyModule";
    }

    @ReactMethod
    public void longMethod(String timeS, Promise promise) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Runnable task = new MyThread(timeS, promise);

        executor.execute(task);
    }

    private class MyThread implements Runnable {
        private Promise promise;
        private int time;

        MyThread(String timeS, Promise newpromise) {
            this.time = Integer.parseInt(timeS);
            this.promise = newpromise;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(this.time);
                this.promise.resolve("Finished sleeping");
            } catch(InterruptedException e) {
                this.promise.reject("Exception while sleeping");
            }
        }
    }
}