⚡ VexChannel

Zero-boilerplate Flutter platform channels.
Write pure Dart. Access every native API on Android, iOS, macOS, Windows, Linux and Web — without writing a single line of Kotlin, Swift, or C++.


Why VexChannel?

Traditional Flutter platform channels require you to:

  1. Write a MethodChannel in Dart
  2. Write matching native code in Kotlin/Java (Android)
  3. Write matching native code in Swift/ObjC (iOS)
  4. Repeat for every platform you target

VexChannel eliminates all of that.

You write Dart. VexChannel handles routing, type coercion, error mapping, retry logic, caching, timeouts, and streaming — for every platform simultaneously.


Installation

dependencies:
  vex_channel: ^1.0.0

Quick Start

Option A — Use a built-in module (zero setup)

import 'package:vex_channel/vex_channel.dart';

// Battery
final battery = VexChannel.bridge(VexBattery.instance);
final level   = await battery.level;           // 87.5
final info    = await battery.info;            // VexBatteryInfo
battery.stream.listen((info) { ... });         // real-time stream

// Device Info
final device = VexChannel.bridge(VexDeviceInfoModule.instance);
final model  = await device.model;             // "Pixel 8 Pro"

// Haptics
final haptics = VexChannel.bridge(VexHaptics.instance);
await haptics.success;                         // 🎉 success haptic

// Clipboard
final cb = VexChannel.bridge(VexClipboard.instance);
await cb.setText('Hello VexChannel!');

// Connectivity
final conn = VexChannel.bridge(VexConnectivity.instance);
print(await conn.isWifi);                      // true
conn.onConnectivityChanged.listen((type) { ... });

// Sensors
final sensors = VexChannel.bridge(VexSensors.instance);
sensors.accelerometer.listen((data) {
  print('x=${data.x} y=${data.y} z=${data.z}');
});

// Storage (key-value + secure)
final store = VexChannel.bridge(VexStorage.instance);
await store.setString('token', 'abc123');
await store.setSecureString('api_key', 's3cr3t');
final token = await store.getString('token');

// File System
final fs   = VexChannel.bridge(VexFileSystem.instance);
final docs = await fs.documentsDirectory;
await fs.writeAsString('$docs/note.txt', 'Hello!');
final text = await fs.readAsString('$docs/note.txt');

// Camera
final cam   = VexChannel.bridge(VexCameraModule.instance);
final photo = await cam.capturePhoto(quality: 90);   // returns file path

// Permissions
final perms = VexChannel.bridge(VexPermissions.instance);
final status = await perms.request(VexPermission.camera);
if (status == VexPermissionStatus.granted) { ... }

// Location
final loc = VexChannel.bridge(VexLocationModule.instance);
final pos = await loc.getCurrentLocation();
print('${pos.latitude}, ${pos.longitude}');

// Biometrics
final bio = VexChannel.bridge(VexBiometrics.instance);
final ok  = await bio.authenticate(reason: 'Confirm your identity');

// Notifications
final notif = VexChannel.bridge(VexNotifications.instance);
await notif.show(VexNotification(id: '1', title: 'Hello', body: 'From VexChannel'));

// Audio
final audio    = VexChannel.bridge(VexAudio.instance);
final playerId = await audio.loadUrl('https://example.com/song.mp3');
await audio.play(playerId);
audio.positionStream(playerId).listen((pos) => print(pos));

Option B — Create your OWN bridge (Dart only, no native changes)

// 1. Extend VexBridgeBase — that's all the Dart side needs
class CameraBridge extends VexBridgeBase {
  @override
  String get channelName => 'com.myapp/camera';

  // 2. Call invokeMethod for one-shot calls
  Future<String?> capturePhoto({int quality = 90}) async {
    final res = await invokeMethod<String>(
      'capturePhoto',
      args: {'quality': quality},
    );
    return res.unwrapOr(null);
  }

  // 3. Call openStream for real-time events
  Stream<Map<String, dynamic>> get previewFrames =>
      openStream<Map<String, dynamic>>(eventName: 'preview');
}

// 4. Register and use
final camera = VexChannel.bridge(CameraBridge());
final path   = await camera.capturePhoto(quality: 95);
camera.previewFrames.listen((frame) { ... });

Option C — Low-level one-shot (no bridge class at all)

// Invoke any native method on any channel in one line
final res = await VexChannel.invoke<double>(
  channel: 'com.myapp/battery',
  method:  'getLevel',
);
print(res.unwrap());   // 87.5

// Or with the shorthand (throws on error)
final level = await VexChannel.call<double>(
  channel: 'vex_channel/battery',
  method:  'getBatteryLevel',
);

Option D — Native → Dart callbacks

// Register a handler that native can call at any time
VexChannel.registerHandler(
  channel: 'com.myapp/push',
  method:  'onMessage',
  handler: (args) async {
    final title = args?['title'];
    print('Push: $title');
    return {'handled': true};
  },
);

Built-in Modules

Module Class Channel
Battery VexBattery vex_channel/battery
Device Info VexDeviceInfoModule vex_channel/device_info
Connectivity VexConnectivity vex_channel/connectivity
Sensors VexSensors vex_channel/sensors
Permissions VexPermissions vex_channel/permissions
Haptics VexHaptics vex_channel/haptics
Clipboard VexClipboard vex_channel/clipboard
Location VexLocationModule vex_channel/location
File System VexFileSystem vex_channel/file_system
Storage VexStorage vex_channel/storage
Notifications VexNotifications vex_channel/notifications
Biometrics VexBiometrics vex_channel/biometrics
Camera VexCameraModule vex_channel/camera
Audio VexAudio vex_channel/audio
Network (HTTP) VexNetworkModule vex_channel/network

VexResponse — never throw, always return

Every invokeMethod returns a VexResponse<T>:

final res = await invokeMethod<double>('getLevel');

// Pattern match
res.when(
  success: (level) => print('Level: $level'),
  failure: (error)  => print('Error: ${error.message}'),
);

// Or unwrap (throws VexException on failure)
final level = res.unwrap();

// Or provide a fallback
final level = res.unwrapOr(0.0);

// Map the value
final percent = res.map((v) => '${v.toStringAsFixed(1)}%');

Error Types

Exception Cause
VexException Generic native error
VexTimeoutException Call exceeded timeout
VexUnsupportedPlatformException Feature not available on current OS
VexPermissionDeniedException Native permission denied

Advanced Features

Retry on transient failure

final res = await invokeMethod<String>(
  'getToken',
  retryCount: 3,   // retry up to 3 times with exponential back-off
);

Per-call caching

final res = await invokeMethod<Map<String, dynamic>>(
  'getDeviceInfo',
  cacheable: true,
  cacheTtl: Duration(hours: 24),  // cached for 24 hours
);

Custom timeout per call

final res = await invokeMethod<String>(
  'authenticate',
  timeout: Duration(minutes: 2),
);

Multi-codec support

@VexBridge('com.myapp/data', codec: VexCodecType.json)
class DataBridge extends VexBridgeBase { ... }

Platform detection

if (VexChannel.isRunningOn(VexOS.android)) {
  // Android-specific path
}

switch (VexChannel.currentPlatform) {
  case VexOS.ios:     ...
  case VexOS.android: ...
  case VexOS.web:     ...
  default:            ...
}

Logging & debugging

VexChannel.enableLogging(verbose: true);
// Prints every channel call with timing:
// [VexChannel] → vex_channel/battery#getBatteryLevel
// [VexChannel] ✓ vex_channel/battery#getBatteryLevel → 3ms

VexChannel.registry.debugDump();
// ╔══════════════════════════════════════
// ║ VexChannel Registry — 3 bridge(s)
// ║  • VexBattery → VexBattery
// ║  • VexConnectivity → VexConnectivity
// ╚══════════════════════════════════════

Writing Native Handlers (one-time setup)

VexChannel ships with complete Android (Kotlin) and iOS (Swift) handler implementations for all built-in modules. You only need to write native code when adding your own custom channel.

Android (Kotlin)

// Register in VexChannelPlugin.kt — one line
MethodChannel(messenger, "com.myapp/camera").setMethodCallHandler { call, result ->
    when (call.method) {
        "capturePhoto" -> {
            val quality = call.argument<Int>("quality") ?: 90
            // ... actual native camera code ...
            result.success("/path/to/photo.jpg")
        }
        else -> result.notImplemented()
    }
}

iOS (Swift)

FlutterMethodChannel(name: "com.myapp/camera", binaryMessenger: messenger)
    .setMethodCallHandler { call, result in
        if call.method == "capturePhoto" {
            // ... actual native camera code ...
            result("/path/to/photo.jpg")
        } else {
            result(FlutterMethodNotImplemented)
        }
    }

That's literally all the native code you ever need — one handler per channel. VexChannel takes care of everything else.


License

MIT © Mysterious Coder

Libraries

vex_channel
VexChannel — Zero-boilerplate Flutter platform channels.
vex_channel_web