Capacitor: Desarrollo de Plugins Nativos Personalizados para iOS y Android

7 de enero de 2026
Osman Jimenez
Capacitor Plugins iOS Android

Capacitor: Creando Plugins Nativos Personalizados

Una de las fortalezas más poderosas de Capacitor es su capacidad para crear plugins nativos personalizados que permiten acceder a funcionalidades específicas de iOS y Android que no están disponibles en los plugins oficiales. En esta guía completa, exploramos el proceso de desarrollo de plugins desde cero, incluyendo mejores prácticas, patrones de diseño y casos de uso avanzados.

Arquitectura de Plugins en Capacitor

Estructura Básica

Un plugin de Capacitor consta de tres componentes principales:

  • Definición TypeScript: Interfaz y tipos para el lado web
  • Implementación iOS: Código Swift/Objective-C
  • Implementación Android: Código Java/Kotlin
// Estructura de archivos de un plugin
my-capacitor-plugin/
├── src/
│   ├── definitions.ts      # Definiciones TypeScript
│   ├── index.ts           # Exportaciones principales
│   └── web.ts             # Implementación web (opcional)
├── ios/
│   ├── Plugin/
│   │   ├── Plugin.swift   # Implementación iOS
│   │   └── Plugin.m       # Bridge Objective-C
│   └── Plugin.podspec     # Especificación CocoaPods
├── android/
│   └── src/main/java/
│       └── com/company/plugin/
│           └── Plugin.java # Implementación Android
├── package.json
└── capacitor.config.json

Creación de un Plugin desde Cero

Inicialización del Proyecto

# Crear plugin usando el CLI oficial
npm install -g @capacitor/cli
npx @capacitor/create-plugin my-awesome-plugin

# O crear manualmente
mkdir capacitor-awesome-plugin
cd capacitor-awesome-plugin
npm init -y

# Instalar dependencias base
npm install @capacitor/core
npm install --save-dev @capacitor/cli @capacitor/docgen typescript

Definición de la Interfaz TypeScript

// src/definitions.ts
export interface AwesomePluginPlugin {
  /**
   * Obtiene información del dispositivo
   */
  getDeviceInfo(): Promise;
  
  /**
   * Muestra una notificación nativa
   */
  showNativeAlert(options: AlertOptions): Promise;
  
  /**
   * Accede a funcionalidad específica del sistema
   */
  accessSystemFeature(options: SystemFeatureOptions): Promise;
  
  /**
   * Escucha eventos nativos
   */
  addListener(
    eventName: 'deviceShaken' | 'batteryChanged',
    listenerFunc: (event: any) => void
  ): Promise;
  
  /**
   * Remueve todos los listeners
   */
  removeAllListeners(): Promise;
}

export interface DeviceInfo {
  model: string;
  manufacturer: string;
  osVersion: string;
  batteryLevel: number;
  isCharging: boolean;
  networkType: 'wifi' | 'cellular' | 'none';
  totalMemory: number;
  freeMemory: number;
}

export interface AlertOptions {
  title: string;
  message: string;
  buttonText?: string;
  type?: 'info' | 'warning' | 'error' | 'success';
}

export interface AlertResult {
  dismissed: boolean;
  timestamp: number;
}

export interface SystemFeatureOptions {
  feature: 'biometric' | 'nfc' | 'bluetooth' | 'camera_flash';
  action: 'check' | 'enable' | 'disable' | 'toggle';
  parameters?: { [key: string]: any };
}

export interface SystemFeatureResult {
  available: boolean;
  enabled: boolean;
  data?: any;
  error?: string;
}

Implementación Web (Fallback)

// src/web.ts
import { WebPlugin } from '@capacitor/core';
import type { 
  AwesomePluginPlugin, 
  DeviceInfo, 
  AlertOptions, 
  AlertResult,
  SystemFeatureOptions,
  SystemFeatureResult
} from './definitions';

export class AwesomePluginWeb extends WebPlugin implements AwesomePluginPlugin {
  
  async getDeviceInfo(): Promise {
    // Implementación usando APIs web cuando sea posible
    const info: DeviceInfo = {
      model: 'Web Browser',
      manufacturer: 'Browser Vendor',
      osVersion: navigator.userAgent,
      batteryLevel: 0,
      isCharging: false,
      networkType: (navigator as any).connection?.effectiveType || 'unknown',
      totalMemory: (performance as any).memory?.jsHeapSizeLimit || 0,
      freeMemory: (performance as any).memory?.usedJSHeapSize || 0
    };
    
    // Intentar obtener información de batería si está disponible
    if ('getBattery' in navigator) {
      try {
        const battery = await (navigator as any).getBattery();
        info.batteryLevel = Math.round(battery.level * 100);
        info.isCharging = battery.charging;
      } catch (error) {
        console.warn('Battery API not available');
      }
    }
    
    return info;
  }
  
  async showNativeAlert(options: AlertOptions): Promise {
    // Fallback a alert() del navegador
    const message = `${options.title}\n\n${options.message}`;
    
    return new Promise((resolve) => {
      const startTime = Date.now();
      
      // Usar confirm() si hay botón personalizado
      if (options.buttonText) {
        const result = confirm(message);
        resolve({
          dismissed: !result,
          timestamp: Date.now() - startTime
        });
      } else {
        alert(message);
        resolve({
          dismissed: true,
          timestamp: Date.now() - startTime
        });
      }
    });
  }
  
  async accessSystemFeature(options: SystemFeatureOptions): Promise {
    // Implementaciones web limitadas
    switch (options.feature) {
      case 'biometric':
        // Verificar si Web Authentication API está disponible
        if ('credentials' in navigator && 'create' in navigator.credentials) {
          return {
            available: true,
            enabled: true,
            data: { type: 'webauthn' }
          };
        }
        break;
        
      case 'bluetooth':
        // Verificar Web Bluetooth API
        if ('bluetooth' in navigator) {
          return {
            available: true,
            enabled: true,
            data: { type: 'web-bluetooth' }
          };
        }
        break;
        
      default:
        return {
          available: false,
          enabled: false,
          error: `Feature '${options.feature}' not available in web`
        };
    }
    
    return {
      available: false,
      enabled: false,
      error: 'Feature not supported in web environment'
    };
  }
}

Implementación iOS (Swift)

Plugin Principal

// ios/Plugin/Plugin.swift
import Foundation
import Capacitor
import UIKit
import CoreMotion

@objc(AwesomePlugin)
public class AwesomePlugin: CAPPlugin {
    private let implementation = AwesomePluginImplementation()
    private var motionManager: CMMotionManager?
    
    override public func load() {
        // Inicialización del plugin
        setupMotionDetection()
        setupBatteryMonitoring()
    }
    
    @objc func getDeviceInfo(_ call: CAPPluginCall) {
        Task {
            do {
                let deviceInfo = await implementation.getDeviceInfo()
                call.resolve(deviceInfo)
            } catch {
                call.reject("Error getting device info: \(error.localizedDescription)")
            }
        }
    }
    
    @objc func showNativeAlert(_ call: CAPPluginCall) {
        guard let title = call.getString("title"),
              let message = call.getString("message") else {
            call.reject("Missing required parameters")
            return
        }
        
        let buttonText = call.getString("buttonText") ?? "OK"
        let type = call.getString("type") ?? "info"
        
        DispatchQueue.main.async {
            self.implementation.showNativeAlert(
                title: title,
                message: message,
                buttonText: buttonText,
                type: type
            ) { result in
                call.resolve(result)
            }
        }
    }
    
    @objc func accessSystemFeature(_ call: CAPPluginCall) {
        guard let feature = call.getString("feature"),
              let action = call.getString("action") else {
            call.reject("Missing required parameters")
            return
        }
        
        let parameters = call.getObject("parameters") ?? [:]
        
        Task {
            do {
                let result = await implementation.accessSystemFeature(
                    feature: feature,
                    action: action,
                    parameters: parameters
                )
                call.resolve(result)
            } catch {
                call.reject("Error accessing system feature: \(error.localizedDescription)")
            }
        }
    }
    
    // MARK: - Motion Detection
    private func setupMotionDetection() {
        motionManager = CMMotionManager()
        
        if motionManager?.isDeviceMotionAvailable == true {
            motionManager?.deviceMotionUpdateInterval = 0.1
            motionManager?.startDeviceMotionUpdates(to: .main) { [weak self] (motion, error) in
                guard let motion = motion else { return }
                
                // Detectar sacudida del dispositivo
                let acceleration = motion.userAcceleration
                let threshold = 2.0
                
                if abs(acceleration.x) > threshold ||
                   abs(acceleration.y) > threshold ||
                   abs(acceleration.z) > threshold {
                    
                    self?.notifyListeners("deviceShaken", data: [
                        "timestamp": Date().timeIntervalSince1970,
                        "acceleration": [
                            "x": acceleration.x,
                            "y": acceleration.y,
                            "z": acceleration.z
                        ]
                    ])
                }
            }
        }
    }
    
    // MARK: - Battery Monitoring
    private func setupBatteryMonitoring() {
        UIDevice.current.isBatteryMonitoringEnabled = true
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batteryLevelChanged),
            name: UIDevice.batteryLevelDidChangeNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(batteryStateChanged),
            name: UIDevice.batteryStateDidChangeNotification,
            object: nil
        )
    }
    
    @objc private func batteryLevelChanged() {
        let level = UIDevice.current.batteryLevel
        let state = UIDevice.current.batteryState
        
        notifyListeners("batteryChanged", data: [
            "level": Int(level * 100),
            "isCharging": state == .charging || state == .full
        ])
    }
    
    @objc private func batteryStateChanged() {
        batteryLevelChanged() // Reutilizar la misma lógica
    }
    
    deinit {
        motionManager?.stopDeviceMotionUpdates()
        NotificationCenter.default.removeObserver(self)
    }
}

// MARK: - Implementation Class
public class AwesomePluginImplementation {
    
    func getDeviceInfo() async -> [String: Any] {
        let device = UIDevice.current
        let processInfo = ProcessInfo.processInfo
        
        // Obtener información de memoria
        let memoryInfo = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout.size)/4
        
        let kerr: kern_return_t = withUnsafeMutablePointer(to: &memoryInfo) {
            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                task_info(mach_task_self_,
                         task_flavor_t(MACH_TASK_BASIC_INFO),
                         $0,
                         &count)
            }
        }
        
        let totalMemory = processInfo.physicalMemory
        let usedMemory = kerr == KERN_SUCCESS ? memoryInfo.resident_size : 0
        
        // Obtener información de red
        let networkType = getNetworkType()
        
        return [
            "model": device.model,
            "manufacturer": "Apple",
            "osVersion": device.systemVersion,
            "batteryLevel": Int(device.batteryLevel * 100),
            "isCharging": device.batteryState == .charging || device.batteryState == .full,
            "networkType": networkType,
            "totalMemory": totalMemory,
            "freeMemory": totalMemory - usedMemory
        ]
    }
    
    func showNativeAlert(
        title: String,
        message: String,
        buttonText: String,
        type: String,
        completion: @escaping ([String: Any]) -> Void
    ) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        
        // Configurar estilo según el tipo
        if #available(iOS 13.0, *) {
            switch type {
            case "error":
                alert.view.tintColor = .systemRed
            case "warning":
                alert.view.tintColor = .systemOrange
            case "success":
                alert.view.tintColor = .systemGreen
            default:
                alert.view.tintColor = .systemBlue
            }
        }
        
        let startTime = Date()
        
        let action = UIAlertAction(title: buttonText, style: .default) { _ in
            let endTime = Date()
            completion([
                "dismissed": true,
                "timestamp": Int(endTime.timeIntervalSince(startTime) * 1000)
            ])
        }
        
        alert.addAction(action)
        
        DispatchQueue.main.async {
            if let viewController = UIApplication.shared.windows.first?.rootViewController {
                viewController.present(alert, animated: true)
            }
        }
    }
    
    func accessSystemFeature(
        feature: String,
        action: String,
        parameters: [String: Any]
    ) async -> [String: Any] {
        
        switch feature {
        case "biometric":
            return await handleBiometricFeature(action: action, parameters: parameters)
        case "camera_flash":
            return handleCameraFlash(action: action)
        case "bluetooth":
            return await handleBluetoothFeature(action: action)
        default:
            return [
                "available": false,
                "enabled": false,
                "error": "Feature '\(feature)' not supported"
            ]
        }
    }
    
    // MARK: - Feature Implementations
    private func handleBiometricFeature(action: String, parameters: [String: Any]) async -> [String: Any] {
        import LocalAuthentication
        
        let context = LAContext()
        var error: NSError?
        
        let available = context.canEvaluatePolicy(.biometryAny, error: &error)
        
        if action == "check" {
            return [
                "available": available,
                "enabled": available,
                "data": [
                    "biometryType": getBiometryType(context: context)
                ]
            ]
        }
        
        if available && action == "enable" {
            do {
                let reason = parameters["reason"] as? String ?? "Authenticate to continue"
                let success = try await context.evaluatePolicy(.biometryAny, localizedReason: reason)
                
                return [
                    "available": true,
                    "enabled": success,
                    "data": ["authenticated": success]
                ]
            } catch {
                return [
                    "available": true,
                    "enabled": false,
                    "error": error.localizedDescription
                ]
            }
        }
        
        return [
            "available": available,
            "enabled": false
        ]
    }
    
    private func handleCameraFlash(action: String) -> [String: Any] {
        import AVFoundation
        
        guard let device = AVCaptureDevice.default(for: .video),
              device.hasTorch else {
            return [
                "available": false,
                "enabled": false,
                "error": "Flash not available"
            ]
        }
        
        do {
            try device.lockForConfiguration()
            
            switch action {
            case "enable":
                try device.setTorchModeOn(level: 1.0)
            case "disable":
                device.torchMode = .off
            case "toggle":
                if device.torchMode == .on {
                    device.torchMode = .off
                } else {
                    try device.setTorchModeOn(level: 1.0)
                }
            }
            
            device.unlockForConfiguration()
            
            return [
                "available": true,
                "enabled": device.torchMode == .on
            ]
        } catch {
            return [
                "available": true,
                "enabled": false,
                "error": error.localizedDescription
            ]
        }
    }
    
    // MARK: - Helper Methods
    private func getNetworkType() -> String {
        // Implementación simplificada
        // En una implementación real, usarías Network framework
        return "wifi" // Placeholder
    }
    
    private func getBiometryType(context: LAContext) -> String {
        if #available(iOS 11.0, *) {
            switch context.biometryType {
            case .faceID:
                return "faceID"
            case .touchID:
                return "touchID"
            default:
                return "none"
            }
        }
        return "unknown"
    }
}

Bridge Objective-C

// ios/Plugin/Plugin.m
#import 
#import 

// Define the plugin using the CAP_PLUGIN Macro, and
// each method the plugin supports using the CAP_PLUGIN_METHOD macro.
CAP_PLUGIN(AwesomePlugin, "AwesomePlugin",
           CAP_PLUGIN_METHOD(getDeviceInfo, CAPPluginReturnPromise);
           CAP_PLUGIN_METHOD(showNativeAlert, CAPPluginReturnPromise);
           CAP_PLUGIN_METHOD(accessSystemFeature, CAPPluginReturnPromise);
)

Implementación Android (Java/Kotlin)

Plugin Principal

// android/src/main/java/com/company/plugin/AwesomePlugin.java
package com.company.plugin;

import android.Manifest;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.BatteryManager;
import android.os.Build;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;

import java.util.HashMap;
import java.util.Map;

@CapacitorPlugin(
    name = "AwesomePlugin",
    permissions = {
        @Permission(strings = { Manifest.permission.CAMERA }, alias = "camera"),
        @Permission(strings = { Manifest.permission.ACCESS_FINE_LOCATION }, alias = "location"),
        @Permission(strings = { Manifest.permission.USE_BIOMETRIC }, alias = "biometric")
    }
)
public class AwesomePlugin extends Plugin implements SensorEventListener {
    
    private AwesomePluginImplementation implementation;
    private SensorManager sensorManager;
    private Sensor accelerometer;
    private BroadcastReceiver batteryReceiver;
    
    @Override
    public void load() {
        implementation = new AwesomePluginImplementation(getContext(), getActivity());
        setupSensors();
        setupBatteryMonitoring();
    }
    
    @PluginMethod
    public void getDeviceInfo(PluginCall call) {
        try {
            JSObject result = implementation.getDeviceInfo();
            call.resolve(result);
        } catch (Exception e) {
            call.reject("Error getting device info: " + e.getMessage());
        }
    }
    
    @PluginMethod
    public void showNativeAlert(PluginCall call) {
        String title = call.getString("title");
        String message = call.getString("message");
        String buttonText = call.getString("buttonText", "OK");
        String type = call.getString("type", "info");
        
        if (title == null || message == null) {
            call.reject("Missing required parameters");
            return;
        }
        
        getActivity().runOnUiThread(() -> {
            implementation.showNativeAlert(title, message, buttonText, type, call);
        });
    }
    
    @PluginMethod
    public void accessSystemFeature(PluginCall call) {
        String feature = call.getString("feature");
        String action = call.getString("action");
        JSObject parameters = call.getObject("parameters", new JSObject());
        
        if (feature == null || action == null) {
            call.reject("Missing required parameters");
            return;
        }
        
        try {
            JSObject result = implementation.accessSystemFeature(feature, action, parameters);
            call.resolve(result);
        } catch (Exception e) {
            call.reject("Error accessing system feature: " + e.getMessage());
        }
    }
    
    // MARK: - Sensor Setup
    private void setupSensors() {
        sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
        if (sensorManager != null) {
            accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            if (accelerometer != null) {
                sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI);
            }
        }
    }
    
    private void setupBatteryMonitoring() {
        batteryReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
                int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
                
                boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                                   status == BatteryManager.BATTERY_STATUS_FULL;
                
                int batteryPct = (int) ((level / (float) scale) * 100);
                
                JSObject data = new JSObject();
                data.put("level", batteryPct);
                data.put("isCharging", isCharging);
                
                notifyListeners("batteryChanged", data);
            }
        };
        
        IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        getContext().registerReceiver(batteryReceiver, filter);
    }
    
    // MARK: - SensorEventListener
    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            float x = event.values[0];
            float y = event.values[1];
            float z = event.values[2];
            
            // Detectar sacudida
            double acceleration = Math.sqrt(x * x + y * y + z * z);
            if (acceleration > 15) { // Threshold para sacudida
                JSObject data = new JSObject();
                data.put("timestamp", System.currentTimeMillis());
                
                JSObject accelerationData = new JSObject();
                accelerationData.put("x", x);
                accelerationData.put("y", y);
                accelerationData.put("z", z);
                data.put("acceleration", accelerationData);
                
                notifyListeners("deviceShaken", data);
            }
        }
    }
    
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // No implementation needed
    }
    
    @Override
    protected void handleOnDestroy() {
        if (sensorManager != null && accelerometer != null) {
            sensorManager.unregisterListener(this);
        }
        
        if (batteryReceiver != null) {
            getContext().unregisterReceiver(batteryReceiver);
        }
        
        super.handleOnDestroy();
    }
}

Implementación de Funcionalidades

// AwesomePluginImplementation.java
package com.company.plugin;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraManager;
import android.os.Build;
import android.provider.Settings;

import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;

import com.getcapacitor.JSObject;
import com.getcapacitor.PluginCall;

import java.util.concurrent.Executor;

public class AwesomePluginImplementation {
    
    private Context context;
    private Activity activity;
    private CameraManager cameraManager;
    private String cameraId;
    
    public AwesomePluginImplementation(Context context, Activity activity) {
        this.context = context;
        this.activity = activity;
        
        // Inicializar camera manager
        cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        try {
            if (cameraManager != null) {
                String[] cameraIds = cameraManager.getCameraIdList();
                if (cameraIds.length > 0) {
                    cameraId = cameraIds[0]; // Usar primera cámara
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    
    public JSObject getDeviceInfo() {
        JSObject result = new JSObject();
        
        result.put("model", Build.MODEL);
        result.put("manufacturer", Build.MANUFACTURER);
        result.put("osVersion", Build.VERSION.RELEASE);
        
        // Información de batería
        BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
        if (batteryManager != null) {
            int batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
            boolean isCharging = batteryManager.isCharging();
            
            result.put("batteryLevel", batteryLevel);
            result.put("isCharging", isCharging);
        }
        
        // Información de memoria
        Runtime runtime = Runtime.getRuntime();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        
        result.put("totalMemory", totalMemory);
        result.put("freeMemory", freeMemory);
        
        // Tipo de red (simplificado)
        result.put("networkType", "unknown"); // Implementar detección real
        
        return result;
    }
    
    public void showNativeAlert(String title, String message, String buttonText, String type, PluginCall call) {
        long startTime = System.currentTimeMillis();
        
        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        builder.setTitle(title)
               .setMessage(message)
               .setPositiveButton(buttonText, (dialog, which) -> {
                   long endTime = System.currentTimeMillis();
                   
                   JSObject result = new JSObject();
                   result.put("dismissed", true);
                   result.put("timestamp", endTime - startTime);
                   
                   call.resolve(result);
               })
               .setCancelable(false);
        
        // Configurar icono según el tipo
        switch (type) {
            case "error":
                builder.setIcon(android.R.drawable.ic_dialog_alert);
                break;
            case "warning":
                builder.setIcon(android.R.drawable.ic_dialog_info);
                break;
            case "success":
                builder.setIcon(android.R.drawable.ic_dialog_info);
                break;
            default:
                builder.setIcon(android.R.drawable.ic_dialog_info);
                break;
        }
        
        AlertDialog dialog = builder.create();
        dialog.show();
    }
    
    public JSObject accessSystemFeature(String feature, String action, JSObject parameters) {
        switch (feature) {
            case "biometric":
                return handleBiometricFeature(action, parameters);
            case "camera_flash":
                return handleCameraFlash(action);
            case "bluetooth":
                return handleBluetoothFeature(action);
            default:
                JSObject error = new JSObject();
                error.put("available", false);
                error.put("enabled", false);
                error.put("error", "Feature '" + feature + "' not supported");
                return error;
        }
    }
    
    private JSObject handleBiometricFeature(String action, JSObject parameters) {
        JSObject result = new JSObject();
        
        BiometricManager biometricManager = BiometricManager.from(context);
        
        switch (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
            case BiometricManager.BIOMETRIC_SUCCESS:
                result.put("available", true);
                
                if ("enable".equals(action)) {
                    // Implementar autenticación biométrica
                    authenticateWithBiometric(result, parameters);
                } else {
                    result.put("enabled", true);
                }
                break;
                
            case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
                result.put("available", false);
                result.put("enabled", false);
                result.put("error", "No biometric hardware available");
                break;
                
            case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
                result.put("available", false);
                result.put("enabled", false);
                result.put("error", "Biometric hardware unavailable");
                break;
                
            case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
                result.put("available", true);
                result.put("enabled", false);
                result.put("error", "No biometric credentials enrolled");
                break;
                
            default:
                result.put("available", false);
                result.put("enabled", false);
                result.put("error", "Biometric authentication not available");
                break;
        }
        
        return result;
    }
    
    private void authenticateWithBiometric(JSObject result, JSObject parameters) {
        if (!(activity instanceof FragmentActivity)) {
            result.put("enabled", false);
            result.put("error", "Activity must extend FragmentActivity");
            return;
        }
        
        FragmentActivity fragmentActivity = (FragmentActivity) activity;
        Executor executor = ContextCompat.getMainExecutor(context);
        
        BiometricPrompt biometricPrompt = new BiometricPrompt(fragmentActivity, executor,
            new BiometricPrompt.AuthenticationCallback() {
                @Override
                public void onAuthenticationError(int errorCode, CharSequence errString) {
                    super.onAuthenticationError(errorCode, errString);
                    result.put("enabled", false);
                    result.put("error", errString.toString());
                }
                
                @Override
                public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authResult) {
                    super.onAuthenticationSucceeded(authResult);
                    result.put("enabled", true);
                    result.put("data", new JSObject().put("authenticated", true));
                }
                
                @Override
                public void onAuthenticationFailed() {
                    super.onAuthenticationFailed();
                    result.put("enabled", false);
                    result.put("error", "Authentication failed");
                }
            });
        
        String reason = parameters.getString("reason", "Authenticate to continue");
        
        BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
            .setTitle("Biometric Authentication")
            .setSubtitle(reason)
            .setNegativeButtonText("Cancel")
            .build();
        
        biometricPrompt.authenticate(promptInfo);
    }
    
    private JSObject handleCameraFlash(String action) {
        JSObject result = new JSObject();
        
        if (cameraManager == null || cameraId == null) {
            result.put("available", false);
            result.put("enabled", false);
            result.put("error", "Camera not available");
            return result;
        }
        
        try {
            boolean hasFlash = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
            
            if (!hasFlash) {
                result.put("available", false);
                result.put("enabled", false);
                result.put("error", "Flash not available");
                return result;
            }
            
            result.put("available", true);
            
            switch (action) {
                case "enable":
                    cameraManager.setTorchMode(cameraId, true);
                    result.put("enabled", true);
                    break;
                case "disable":
                    cameraManager.setTorchMode(cameraId, false);
                    result.put("enabled", false);
                    break;
                case "toggle":
                    // Para simplificar, asumimos que está apagado y lo encendemos
                    cameraManager.setTorchMode(cameraId, true);
                    result.put("enabled", true);
                    break;
                default:
                    result.put("enabled", false);
                    break;
            }
            
        } catch (CameraAccessException e) {
            result.put("available", true);
            result.put("enabled", false);
            result.put("error", e.getMessage());
        }
        
        return result;
    }
    
    private JSObject handleBluetoothFeature(String action) {
        JSObject result = new JSObject();
        
        // Implementación básica de Bluetooth
        boolean hasBluetoothFeature = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
        
        result.put("available", hasBluetoothFeature);
        result.put("enabled", hasBluetoothFeature);
        
        if (!hasBluetoothFeature) {
            result.put("error", "Bluetooth not available");
        }
        
        return result;
    }
}

Configuración y Distribución

Package.json

{
  "name": "capacitor-awesome-plugin",
  "version": "1.0.0",
  "description": "An awesome Capacitor plugin with native features",
  "main": "dist/plugin.cjs.js",
  "module": "dist/esm/index.js",
  "types": "dist/esm/index.d.ts",
  "unpkg": "dist/plugin.js",
  "files": [
    "android/src/main/",
    "android/build.gradle",
    "dist/",
    "ios/Plugin/",
    "CapacitorAwesomePlugin.podspec"
  ],
  "author": "Your Name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/yourusername/capacitor-awesome-plugin.git"
  },
  "bugs": {
    "url": "https://github.com/yourusername/capacitor-awesome-plugin/issues"
  },
  "keywords": [
    "capacitor",
    "plugin",
    "native",
    "ios",
    "android"
  ],
  "scripts": {
    "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
    "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS",
    "verify:android": "cd android && ./gradlew clean build test",
    "verify:web": "npm run build",
    "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
    "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
    "eslint": "eslint . --ext ts",
    "prettier": "prettier \"**/*.{css,html,ts,js,java}\"",
    "swiftlint": "node-swiftlint",
    "docgen": "docgen --api AwesomePluginPlugin --output-readme README.md --output-json dist/docs.json",
    "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js",
    "clean": "rimraf ./dist",
    "watch": "tsc --watch",
    "prepublishOnly": "npm run build"
  },
  "devDependencies": {
    "@capacitor/android": "^5.0.0",
    "@capacitor/core": "^5.0.0",
    "@capacitor/docgen": "^0.0.18",
    "@capacitor/ios": "^5.0.0",
    "@ionic/eslint-config": "^0.3.0",
    "@ionic/prettier-config": "^1.0.1",
    "@ionic/swiftlint-config": "^1.1.2",
    "eslint": "^7.11.0",
    "prettier": "~2.3.0",
    "prettier-plugin-java": "~1.0.2",
    "rimraf": "^3.0.2",
    "rollup": "^2.32.0",
    "swiftlint": "^1.0.1",
    "typescript": "~4.1.5"
  },
  "peerDependencies": {
    "@capacitor/core": "^5.0.0"
  },
  "prettier": "@ionic/prettier-config",
  "swiftlint": "@ionic/swiftlint-config",
  "eslintConfig": {
    "extends": "@ionic/eslint-config/recommended"
  },
  "capacitor": {
    "ios": {
      "src": "ios"
    },
    "android": {
      "src": "android/src/main/java/com/company/plugin"
    }
  }
}

Podspec para iOS

# CapacitorAwesomePlugin.podspec
require 'json'

package = JSON.parse(File.read(File.join(__dir__, 'package.json')))

Pod::Spec.new do |s|
  s.name = 'CapacitorAwesomePlugin'
  s.version = package['version']
  s.summary = package['description']
  s.license = package['license']
  s.homepage = package['repository']['url']
  s.author = package['author']
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
  s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
  s.ios.deployment_target  = '13.0'
  s.dependency 'Capacitor'
  s.swift_version = '5.1'
end

Testing y Debugging

Testing del Plugin

// test-app/src/app/home/home.page.ts
import { Component } from '@angular/core';
import { AwesomePlugin } from 'capacitor-awesome-plugin';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  deviceInfo: any = null;
  
  constructor() {
    this.setupListeners();
  }
  
  async testGetDeviceInfo() {
    try {
      this.deviceInfo = await AwesomePlugin.getDeviceInfo();
      console.log('Device info:', this.deviceInfo);
    } catch (error) {
      console.error('Error getting device info:', error);
    }
  }
  
  async testShowAlert() {
    try {
      const result = await AwesomePlugin.showNativeAlert({
        title: 'Test Alert',
        message: 'This is a test alert from the plugin',
        buttonText: 'Got it!',
        type: 'success'
      });
      console.log('Alert result:', result);
    } catch (error) {
      console.error('Error showing alert:', error);
    }
  }
  
  async testBiometric() {
    try {
      const result = await AwesomePlugin.accessSystemFeature({
        feature: 'biometric',
        action: 'enable',
        parameters: {
          reason: 'Please authenticate to test biometric feature'
        }
      });
      console.log('Biometric result:', result);
    } catch (error) {
      console.error('Error with biometric:', error);
    }
  }
  
  async testFlash() {
    try {
      const result = await AwesomePlugin.accessSystemFeature({
        feature: 'camera_flash',
        action: 'toggle'
      });
      console.log('Flash result:', result);
    } catch (error) {
      console.error('Error with flash:', error);
    }
  }
  
  private setupListeners() {
    // Listener para sacudida del dispositivo
    AwesomePlugin.addListener('deviceShaken', (event) => {
      console.log('Device shaken!', event);
      // Mostrar feedback visual
    });
    
    // Listener para cambios de batería
    AwesomePlugin.addListener('batteryChanged', (event) => {
      console.log('Battery changed:', event);
      // Actualizar UI con nueva información de batería
    });
  }
}

Mejores Prácticas

1. Diseño de API

  • Consistencia: Usa patrones similares en todas las plataformas
  • Async/await: Todas las operaciones nativas deben ser asíncronas
  • Error handling: Maneja errores de forma consistente
  • Documentación: Documenta todos los métodos y parámetros

2. Performance

  • Lazy loading: Inicializa recursos solo cuando sea necesario
  • Memory management: Libera recursos correctamente
  • Background tasks: Usa threads apropiados para operaciones pesadas

3. Seguridad

  • Permisos: Solicita solo los permisos necesarios
  • Validación: Valida todos los parámetros de entrada
  • Datos sensibles: No logs información sensible

Conclusión

El desarrollo de plugins nativos para Capacitor abre un mundo de posibilidades para acceder a funcionalidades específicas de iOS y Android que no están disponibles en los plugins oficiales. Con una arquitectura bien diseñada y siguiendo las mejores prácticas, puedes crear plugins robustos y reutilizables que extiendan significativamente las capacidades de tus aplicaciones híbridas.

La clave está en mantener una API consistente entre plataformas, manejar errores apropiadamente y optimizar el rendimiento para ofrecer una experiencia nativa de calidad.

¿Has desarrollado plugins personalizados para Capacitor? ¿Qué funcionalidades nativas has necesitado implementar? Comparte tu experiencia en los comentarios.