0%
[/]JOSHIDEAS
SYSTEM_BLOG
TIME:13:33:18
STATUS:ONLINE
~/blog/capacitor-android-native-plugins
$cat capacitor-android-native-plugins.md_

Capacitor.js: Bridging React PWA to Android Native

#Capacitor#Android#React

# Capacitor.js: Bridging React PWA to Android Native

When web APIs aren't enough, Capacitor provides the bridge to native Android functionality. This guide covers building custom plugins for camera, GPS, and Firebase Cloud Messaging.

## Understanding the Bridge

Capacitor creates a two-way communication channel between your web code and native Android:

┌─────────────────────┐     ┌─────────────────────┐
│    Web (React)      │     │   Native (Kotlin)   │
├─────────────────────┤     ├─────────────────────┤
│                     │     │                     │
│  Capacitor.Plugins  │◀───▶│  @CapacitorPlugin   │
│  .MyPlugin.method() │     │  fun method()       │
│                     │     │                     │
└─────────────────────┘     └─────────────────────┘
         │                           │
         └───────── JSON ────────────┘

## Custom Camera Plugin

### Native Side (Kotlin)

kotlin
@CapacitorPlugin( name = "CustomCamera", permissions = [ Permission( strings = [Manifest.permission.CAMERA], alias = "camera" ) ] ) class CustomCameraPlugin : Plugin() { @PluginMethod fun takePhoto(call: PluginCall) { val quality = call.getInt("quality", 90) if (getPermissionState("camera") != PermissionState.GRANTED) { requestPermissionForAlias("camera", call, "cameraPermissionCallback") return } val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) startActivityForResult(call, intent, "handlePhotoResult") } @ActivityCallback private fun handlePhotoResult(call: PluginCall, result: ActivityResult) { if (result.resultCode == Activity.RESULT_OK) { val bitmap = result.data?.extras?.get("data") as Bitmap val base64 = bitmapToBase64(bitmap) val ret = JSObject() ret.put("base64", base64) ret.put("format", "jpeg") call.resolve(ret) } else { call.reject("Photo capture cancelled") } } private fun bitmapToBase64(bitmap: Bitmap): String { val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) val bytes = stream.toByteArray() return Base64.encodeToString(bytes, Base64.NO_WRAP) } }

### Web Side (TypeScript)

typescript
import { registerPlugin } from '@capacitor/core'; export interface CustomCameraPlugin { takePhoto(options?: { quality?: number }): Promise<{ base64: string; format: string; }>; } const CustomCamera = registerPlugin<CustomCameraPlugin>('CustomCamera'); export default CustomCamera;

### React Usage

jsx
import CustomCamera from './plugins/CustomCamera'; function CameraButton() { const [photo, setPhoto] = useState(null); const handleCapture = async () => { try { const result = await CustomCamera.takePhoto({ quality: 85 }); setPhoto(`data:image/jpeg;base64,${result.base64}`); } catch (error) { console.error('Camera error:', error); } }; return ( <div> <button onClick={handleCapture}>Take Photo</button> {photo && <img src={photo} alt="Captured" />} </div> ); }

## GPS Location Plugin

High-accuracy location tracking with battery optimization:

kotlin
@CapacitorPlugin(name = "CustomLocation") class CustomLocationPlugin : Plugin() { private var locationClient: FusedLocationProviderClient? = null private var locationCallback: LocationCallback? = null @PluginMethod fun startTracking(call: PluginCall) { val interval = call.getLong("interval", 5000) locationClient = LocationServices.getFusedLocationProviderClient(activity) val request = LocationRequest.create().apply { this.interval = interval fastestInterval = interval / 2 priority = LocationRequest.PRIORITY_HIGH_ACCURACY } locationCallback = object : LocationCallback() { override fun onLocationResult(result: LocationResult) { result.lastLocation?.let { location -> val data = JSObject() data.put("latitude", location.latitude) data.put("longitude", location.longitude) data.put("accuracy", location.accuracy) data.put("timestamp", location.time) notifyListeners("locationUpdate", data) } } } locationClient?.requestLocationUpdates( request, locationCallback!!, Looper.getMainLooper() ) call.resolve() } @PluginMethod fun stopTracking(call: PluginCall) { locationCallback?.let { locationClient?.removeLocationUpdates(it) } call.resolve() } }

## Firebase Cloud Messaging

Push notifications require both native and web handling:

### Native Registration

kotlin
@CapacitorPlugin(name = "CustomFCM") class CustomFCMPlugin : Plugin() { @PluginMethod fun getToken(call: PluginCall) { FirebaseMessaging.getInstance().token .addOnCompleteListener { task -> if (task.isSuccessful) { val ret = JSObject() ret.put("token", task.result) call.resolve(ret) } else { call.reject("Failed to get FCM token") } } } @PluginMethod fun subscribeToTopic(call: PluginCall) { val topic = call.getString("topic") ?: run { call.reject("Topic required") return } FirebaseMessaging.getInstance() .subscribeToTopic(topic) .addOnCompleteListener { task -> if (task.isSuccessful) { call.resolve() } else { call.reject("Subscription failed") } } } }

### Message Service

kotlin
class CustomFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { message.notification?.let { notification -> showNotification( notification.title ?: "", notification.body ?: "" ) } // Forward data payload to web layer message.data.isNotEmpty().let { val intent = Intent("FCM_MESSAGE") intent.putExtra("data", JSONObject(message.data as Map<*, *>).toString()) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } } override fun onNewToken(token: String) { // Send to your server sendTokenToServer(token) } }

## Plugin Registration

Register plugins in MainActivity.java:

java
public class MainActivity extends BridgeActivity { @Override public void onCreate(Bundle savedInstanceState) { registerPlugin(CustomCameraPlugin.class); registerPlugin(CustomLocationPlugin.class); registerPlugin(CustomFCMPlugin.class); super.onCreate(savedInstanceState); } }

## Testing Strategies

ComponentTest Method
Plugin InterfaceJest mocks
Native CodeAndroid Instrumented Tests
IntegrationReal device testing
PermissionsManual testing

## Common Pitfalls

  1. >Permission handling - Always check before accessing protected features
  2. >Lifecycle management - Clean up resources in handleOnDestroy
  3. >Thread safety - Native callbacks may not be on main thread
  4. >Error serialization - Exceptions must be converted to strings

## Conclusion

Capacitor's plugin architecture cleanly separates web and native concerns while providing seamless communication. Custom plugins unlock device capabilities that web APIs can't reach, enabling truly native experiences from your React codebase.