Base classes and utilities for developing custom Capacitor plugins with cross-platform support. The plugin development API provides a foundation for creating plugins that work seamlessly across web, iOS, and Android platforms.
Base interface that all Capacitor plugins must implement for event handling and lifecycle management.
/**
* Base interface all plugins must implement
*/
interface Plugin {
/** Add event listener and return handle for removal */
addListener(eventName: string, listenerFunc: (...args: any[]) => any): Promise<PluginListenerHandle>;
/** Remove all event listeners */
removeAllListeners(): Promise<void>;
}
/**
* Handle for managing plugin event listeners
*/
interface PluginListenerHandle {
/** Remove this specific listener */
remove(): Promise<void>;
}Base class for implementing web platform plugin functionality with built-in event management and utility methods.
/**
* Base class web plugins should extend
*/
class WebPlugin implements Plugin {
/** Add event listener with automatic management */
addListener(eventName: string, listenerFunc: ListenerCallback): Promise<PluginListenerHandle>;
/** Remove all listeners and clean up resources */
removeAllListeners(): Promise<void>;
/** Notify all listeners for an event (protected) */
protected notifyListeners(eventName: string, data: any, retainUntilConsumed?: boolean): void;
/** Check if any listeners exist for an event (protected) */
protected hasListeners(eventName: string): boolean;
/** Register window event listener (protected) */
protected registerWindowListener(windowEventName: string, pluginEventName: string): void;
/** Create unimplemented error (protected) */
protected unimplemented(msg?: string): CapacitorException;
/** Create unavailable error (protected) */
protected unavailable(msg?: string): CapacitorException;
}
/**
* Callback function type for event listeners
*/
type ListenerCallback = (err: any, ...args: any[]) => void;Usage Examples:
import { WebPlugin } from "@capacitor/core";
// Create a web implementation
class MyPluginWeb extends WebPlugin {
async echo(options: { value: string }): Promise<{ value: string }> {
return { value: options.value };
}
async startListening(): Promise<void> {
// Register window event listener
this.registerWindowListener('resize', 'windowResize');
}
private handleResize = (event: Event) => {
// Notify plugin listeners
this.notifyListeners('windowResize', {
width: window.innerWidth,
height: window.innerHeight,
});
};
async methodNotAvailableOnWeb(): Promise<void> {
throw this.unimplemented('This method is not available on web');
}
async methodRequiringPermission(): Promise<void> {
if (!navigator.geolocation) {
throw this.unavailable('Geolocation not available');
}
// Implementation here
}
}
// Register the plugin
import { registerPlugin } from "@capacitor/core";
interface MyPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
startListening(): Promise<void>;
}
const MyPlugin = registerPlugin<MyPlugin>('MyPlugin', {
web: () => new MyPluginWeb(),
});
// Use the plugin
const result = await MyPlugin.echo({ value: 'Hello' });
console.log(result.value); // 'Hello'
// Add event listener
const listener = await MyPlugin.addListener('windowResize', (data) => {
console.log(`Window resized: ${data.width}x${data.height}`);
});
// Remove specific listener
await listener.remove();
// Or remove all listeners
await MyPlugin.removeAllListeners();Advanced event management system for handling plugin events with retention and window integration.
/**
* Notify all listeners for a specific event
* @param eventName Name of the event to fire
* @param data Data to send to listeners
* @param retainUntilConsumed Keep event data until listeners are added
*/
protected notifyListeners(eventName: string, data: any, retainUntilConsumed?: boolean): void;
/**
* Check if any listeners exist for an event
* @param eventName Name of the event to check
* @returns true if listeners exist, false otherwise
*/
protected hasListeners(eventName: string): boolean;
/**
* Register a window event listener that forwards to plugin listeners
* @param windowEventName Native window event name
* @param pluginEventName Plugin event name to forward to
*/
protected registerWindowListener(windowEventName: string, pluginEventName: string): void;
/**
* Window listener handle for managing native event binding
*/
interface WindowListenerHandle {
registered: boolean;
windowEventName: string;
pluginEventName: string;
handler: (event: any) => void;
}Usage Examples:
class MyAdvancedPluginWeb extends WebPlugin {
constructor() {
super();
// Register window events to forward to plugin events
this.registerWindowListener('online', 'networkChanged');
this.registerWindowListener('offline', 'networkChanged');
}
async startMonitoring(): Promise<void> {
// Simulate retained event - will be delivered to future listeners
this.notifyListeners('initialStatus', {
isOnline: navigator.onLine,
timestamp: Date.now(),
}, true); // retainUntilConsumed = true
}
async sendUpdate(): Promise<void> {
// Only send if someone is listening
if (this.hasListeners('update')) {
this.notifyListeners('update', {
data: 'New update available',
timestamp: Date.now(),
});
}
}
// Window events are automatically forwarded
private handleNetworkChange = (event: Event) => {
this.notifyListeners('networkChanged', {
isOnline: event.type === 'online',
timestamp: Date.now(),
});
};
}Built-in utilities for creating standard plugin exceptions with appropriate error codes.
/**
* Create an unimplemented exception for unsupported methods
* @param msg Custom error message (default: 'not implemented')
* @returns CapacitorException with Unimplemented code
*/
protected unimplemented(msg?: string): CapacitorException;
/**
* Create an unavailable exception for temporarily unavailable features
* @param msg Custom error message (default: 'not available')
* @returns CapacitorException with Unavailable code
*/
protected unavailable(msg?: string): CapacitorException;Usage Examples:
class MyPluginWeb extends WebPlugin {
async nativeOnlyMethod(): Promise<void> {
// Throw unimplemented for features not available on web
throw this.unimplemented('This method is only available on native platforms');
}
async cameraMethod(): Promise<void> {
// Check for required APIs
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw this.unavailable('Camera access not available in this browser');
}
try {
// Implementation here
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// Use stream...
} catch (error) {
throw this.unavailable('Camera permission denied or not available');
}
}
async locationMethod(): Promise<void> {
if (!navigator.geolocation) {
throw this.unavailable('Geolocation not supported');
}
// Check if HTTPS is required
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
throw this.unavailable('Geolocation requires HTTPS');
}
// Implementation here
}
}Common patterns for registering plugins with platform-specific implementations.
Usage Examples:
import { registerPlugin } from "@capacitor/core";
// Pattern 1: Direct implementation objects
const SimplePlugin = registerPlugin<SimplePluginInterface>('SimplePlugin', {
web: new SimplePluginWeb(),
android: new SimplePluginAndroid(), // If pre-loaded
});
// Pattern 2: Lazy-loaded implementations
const LazyPlugin = registerPlugin<LazyPluginInterface>('LazyPlugin', {
web: () => import('./lazy-plugin-web').then(m => new m.LazyPluginWeb()),
android: () => import('./lazy-plugin-android').then(m => new m.LazyPluginAndroid()),
});
// Pattern 3: Factory functions
const FactoryPlugin = registerPlugin<FactoryPluginInterface>('FactoryPlugin', {
web: async () => {
const config = await loadConfiguration();
return new FactoryPluginWeb(config);
},
});
// Pattern 4: Conditional implementations
const ConditionalPlugin = registerPlugin<ConditionalPluginInterface>('ConditionalPlugin', {
web: () => {
if (supportsAdvancedFeatures()) {
return new AdvancedPluginWeb();
} else {
return new BasicPluginWeb();
}
},
});
// Pattern 5: Native-only plugin (no web implementation)
const NativeOnlyPlugin = registerPlugin<NativeOnlyPluginInterface>('NativeOnlyPlugin');
// Will throw CapacitorException with Unimplemented code on webRecommended patterns for testing plugin implementations.
Usage Examples:
// Test web implementation
import { MyPluginWeb } from './my-plugin-web';
describe('MyPluginWeb', () => {
let plugin: MyPluginWeb;
beforeEach(() => {
plugin = new MyPluginWeb();
});
afterEach(async () => {
await plugin.removeAllListeners();
});
it('should echo value', async () => {
const result = await plugin.echo({ value: 'test' });
expect(result.value).toBe('test');
});
it('should handle listeners', async () => {
const callback = jest.fn();
const listener = await plugin.addListener('testEvent', callback);
// Trigger event
plugin.notifyListeners('testEvent', { data: 'test' });
expect(callback).toHaveBeenCalledWith({ data: 'test' });
await listener.remove();
});
it('should throw unimplemented for unsupported methods', async () => {
await expect(plugin.nativeOnlyMethod()).rejects.toThrow();
});
});