Membuat custom plugin Capacitor dari nol: bridge API dengan anotasi `@CAPPlugin` (Swift) dan `@CapacitorPlugin` (Kotlin), method invocation & callback, definisi TypeScript types, serta praktik plugin native sederhana yang dipakai dari JavaScript.

Setelah di episode 11 kita menguasai UI shell (splash screen, status bar, keyboard), pada episode ini kita naik level: menulis plugin native sendiri. Plugin custom adalah kekuatan sejati Capacitor — ketika plugin yang ada tidak memenuhi kebutuhan, kalian bisa menulis kode Swift dan Kotlin langsung dan memanggilnya dari JavaScript.
Mengapa menulis plugin sendiri? Karena ekosistem plugin tidak selalu lengkap. Beberapa kebutuhan spesifik — seperti integrasi SDK native tertentu, akses API hardware yang tidak didukung plugin resmi, atau optimasi performa untuk use case spesifik — membutuhkan kode native.
Setiap plugin Capacitor terdiri dari:
import { registerPlugin } from '@capacitor/core';
export interface DeviceInfoPlugin {
getDeviceInfo(): Promise<{
model: string;
systemVersion: string;
batteryLevel: number;
}>;
}
const DeviceInfo = registerPlugin<DeviceInfoPlugin>('DeviceInfo');
export default DeviceInfo;ios/App/App/DeviceInfoPlugin.swiftimport Capacitor
@objc(DeviceInfoPlugin)
public class DeviceInfoPlugin: CAPPlugin {
@objc func getDeviceInfo(_ call: CAPPluginCall) {
let device = UIDevice.current
let batteryLevel = device.batteryLevel
call.resolve([
"model": device.model,
"systemVersion": device.systemVersion,
"batteryLevel": batteryLevel >= 0 ? batteryLevel : 0,
])
}
}@objc(DeviceInfoPlugin) — mendaftarkan class ke Objective-C runtime agar bisa dipanggil oleh bridge.CAPPlugin — base class dari semua plugin Capacitor di iOS.@objc func — method harus menggunakan @objc agar bisa dipanggil dari bridge.call.resolve(...) — mengembalikan success response ke JavaScript.call.reject(...) — mengembalikan error ke JavaScript.Di AppDelegate.swift:
import Capacitor
@UIApplicationMain
class AppDelegate: CAPBridgingAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Plugin terdaftar otomatis jika file ada di target
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}android/app/src/main/java/com/example/app/DeviceInfoPlugin.ktpackage com.example.app
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
@CapacitorPlugin(name = "DeviceInfo")
class DeviceInfoPlugin : Plugin() {
@PluginMethod
fun getDeviceInfo(call: PluginCall) {
val batteryManager = context.getSystemService(BatteryManager::class.java)
val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) / 100f
val result = JSObject()
result.put("model", Build.MODEL)
result.put("systemVersion", Build.VERSION.RELEASE)
result.put("batteryLevel", batteryLevel)
call.resolve(result)
}
}@CapacitorPlugin(name = "DeviceInfo") — mendaftarkan plugin dengan nama yang cocok dengan TypeScript.Plugin() — base class dari semua plugin Capacitor di Android.@PluginMethod — menandai method sebagai callable dari JavaScript.call.resolve(result) — mengembalikan success response.call.reject("message") — mengembalikan error.Di MainActivity.java:
package com.example.app;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
// Daftarkan plugin custom
this.bridge.registerPlugin(DeviceInfoPlugin.class);
super.onCreate(savedInstanceState);
}
}Jika plugin membutuhkan permission:
@objc func checkPermission(_ call: CAPPluginCall) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
call.resolve(["granted": true])
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
call.resolve(["granted": granted])
}
default:
call.resolve(["granted": false])
}
}@PluginMethod
fun checkPermission(call: PluginCall) {
val granted = ContextCompat.checkSelfPermission(
context, Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
call.resolve(JSObject().put("granted", granted))
}npm run build
npx cap sync
npx cap run iosDi JavaScript:
import DeviceInfo from './definitions';
async function showDeviceInfo() {
const info = await DeviceInfo.getDeviceInfo();
console.log('Model:', info.model);
console.log('OS:', info.systemVersion);
console.log('Battery:', info.batteryLevel);
}Tip
Untuk development, gunakan npx cap open ios atau npx cap open android untuk membuka project di IDE native. Debugging langsung di Xcode/Android Studio lebih cepat daripada console.log dari JavaScript.
Pada episode 12 ini, kalian telah memahami:
@CAPPlugin (Swift) dan @CapacitorPlugin (Kotlin) untuk registrasi.call.resolve() untuk success, call.reject() untuk error.Di episode 13 selanjutnya, kita akan membahas kustomisasi proyek native — Info.plist permissions, AndroidManifest, Gradle dependencies, dan filosofi editing langsung proyek native sebagai source of truth. Sampai jumpa!