Capacitor: Desarrollo de Plugins Nativos Personalizados para iOS y 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.jsonCreació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 typescriptDefinició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'
endTesting 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.