Reliable Bluetooth LE in React Native: A Hybrid Native Module Approach

Reliable Bluetooth LE in React Native: A Hybrid Native Module Approach
JS React Native BRIDGE Foreground Service

Connecting to Bluetooth Low Energy (BLE) devices in React Native is a common requirement for IoT and medical apps. However, developers often face a major hurdle: maintaining a stable connection when the app moves to the background.

In this post, we’ll explore a robust architectural pattern that combines high-level React Native libraries with custom Native Modules to ensure your BLE connections stay alive, no matter what the OS decides to do.


The Problem: The "Background Death" of JS

React Native runs in a JavaScript thread. When a user minimizes your app, the mobile OS (especially Android) aggressively manages resources. If the OS decides your app is consuming too much memory or has been idle in the background, it kills the JS engine.

When the JS engine dies, your BLE connection—managed by libraries like react-native-ble-manager—dies with it.

The Solution: A Hybrid Lifecycle Managed by Native Modules

Instead of trying to rewrite the entire BLE stack in Native code, we use a hybrid approach:

  1. React Native Layer: Handles the business logic, scanning, and data exchange using a standard library.
  2. Native Layer (Lifeline): A custom Native Module that starts a "Foreground Service" (Android) or "Background Task" (iOS) to keep the app process alive.

1. Android: The Foreground Service

On Android, a Foreground Service is a service that the user is actively aware of (via a persistent notification). This tells the OS, "I am doing something important, please don't kill me."

The Native Service (BleConnectionService.java)

We create a service that calls startForeground(). This service doesn't need to handle the Bluetooth GATT connection itself; its presence alone keeps the app's process at a higher priority.

public class BleConnectionService extends Service {
    private static final int NOTIFICATION_ID = 1001;

    @Override
    public void onCreate() {
        super.onCreate();
        // Create notification channel and start foreground
        startForeground(NOTIFICATION_ID, createNotification());
    }

    private Notification createNotification() {
        // Return a notification telling the user the app is maintaining a connection
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // This keeps the service running even if the UI is closed
        return START_REDELIVER_INTENT;
    }
}

The Bridge (BleConnectionModule.java)

We then expose a method to React Native to start this service once a device is connected.

@ReactMethod
public void startConnectionService(String deviceAddress, Promise promise) {
    Intent serviceIntent = new Intent(reactContext, BleConnectionService.class);
    serviceIntent.putExtra("device_address", deviceAddress);
    reactContext.startForegroundService(serviceIntent);
    promise.resolve("Service started");
}

2. iOS: Background Tasks & CoreBluetooth

iOS handles backgrounding differently. While CoreBluetooth has built-in background modes, you often need to wrap your connection logic in a background task to ensure the JS thread finishes its work before being suspended.

The Swift Module (BleConnectionModule.swift)

On iOS, we use beginBackgroundTask to request extra execution time when the app enters the background.

@objc(BleConnectionModule)
class BleConnectionModule: NSObject {
    private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid

    @objc private func appDidEnterBackground() {
        backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "BLEKeepAlive") {
            self.endBackgroundTask()
        }
    }

    private func endBackgroundTask() {
        UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
        backgroundTaskIdentifier = .invalid
    }
}

3. Putting it All Together in React Native

In your React code, you coordinate the library connection and the native lifeline. A common pattern is to use a Context Provider or a custom hook.

// Inside your Connection Hook
const connectToDevice = async (deviceId) => {
  try {
    // 1. Connect using your favorite BLE library
    await BleManager.connect(deviceId);
    
    // 2. Trigger the Native Lifeline
    if (Platform.OS === 'android') {
      await BleConnectionModule.startConnectionService(deviceId);
    } else {
      // iOS background logic is often handled via app state listeners
    }
    
    console.log("Connection secured with native lifeline");
  } catch (error) {
    console.error("Connection failed", error);
  }
};

Key Benefits of This Architecture

  • Separation of Concerns: Your Bluetooth logic stays in JavaScript, where it's easy to test and update.
  • Reliability: By using a Foreground Service, you significantly reduce the "random" disconnects caused by Android's battery optimization.
  • User Transparency: The persistent notification on Android builds trust, letting users know why the app is running in the background.

Conclusion

Handling BLE in React Native requires thinking outside the JavaScript sandbox. By implementing a simple native "shell" to protect your app's process, you can create a seamless and reliable experience for your users, ensuring their devices stay connected even when the phone is in their pocket.

Post a Comment

Previous Post Next Post