Skip to Content
DocsAdd WebF To FlutterBuild Native Plugins

Build Native Plugins

WebF’s module system lets you expose Dart APIs and Flutter plugins to JavaScript, enabling your WebF apps to access native device features, call Flutter plugins, and share complex business logic between Dart and JavaScript.

Overview of Exposing Dart Services

WebF’s module system enables WebF apps to:

  • Access native device features (camera, GPS, sensors)
  • Call Flutter plugins (shared_preferences, path_provider, etc.)
  • Interact with platform-specific APIs
  • Share complex business logic between Dart and JS

Architecture

JavaScript call → Bridge → Dart Module → Flutter Plugin/Service

Automatic Binding Generation with webf module-codegen

Recommended for production. The CLI generates type-safe bindings for Flutter modules (like webf_share and webf_deeplink) and their npm packages.

High-level Flow

  • Describe your module API in a *.module.d.ts file inside the Flutter package
  • Run webf module-codegen:
    • Generates a typed npm wrapper (e.g. @openwebf/webf-share)
    • Generates Dart bindings (*_module_bindings_generated.dart) you extend

Step 1: Add a *.module.d.ts File to Your Module

Create a TypeScript definition next to your Dart module implementation. For example, for the share module:

// lib/src/share.module.d.ts interface ShareTextOptions { title?: string; text?: string; url?: string; } interface ShareSaveResult { success: string; filePath?: string; platformInfo?: string; message: string; error?: string; } interface WebFShare { share( imageData: ArrayBuffer | Uint8Array, text: string, subject?: string ): Promise<boolean>; shareText(options: ShareTextOptions): Promise<boolean>; save( imageData: ArrayBuffer | Uint8Array, filename?: string ): Promise<ShareSaveResult>; saveForPreview( imageData: ArrayBuffer | Uint8Array, filename?: string ): Promise<ShareSaveResult>; }

Similarly for the deeplink module (lib/src/deeplink.module.d.ts), you can describe WebFDeepLink with openDeepLink and registerDeepLinkHandler and their typed options/results.

Step 2: Run webf module-codegen

From the Flutter module directory (for example opensource/webf_modules/share):

# Inside the module folder webf module-codegen

Behavior:

  • Auto-detects the Flutter package via pubspec.yaml (or use --flutter-package-src=<path> to override)
  • Prompts you for an npm package name (e.g. @openwebf/webf-share)
  • Generates:
    • A temporary npm project with:
      • src/index.ts – typed wrapper that calls webf.invokeModuleAsync('Share', ...)
      • src/types.ts – exported TS interfaces such as ShareTextOptions, ShareSaveResult
    • Dart bindings:
      • lib/src/share_module_bindings_generated.dart including:
        • ShareModuleBindings
        • ShareTextOptions, ShareSaveResult, etc.
  • Optionally, it can publish the npm package:
    • --publish-to-npm publishes directly
    • Without the flag, it asks:
      • “Would you like to publish this module package to npm?”
      • Optional custom registry URL

You can also specify an explicit output directory instead of a temp dir:

webf module-codegen ../packages/webf-share \ --flutter-package-src=. \ --package-name=@openwebf/webf-share

Step 3: Extend the Generated Dart Bindings

The generator produces an abstract base for your module, for example Share:

// lib/src/share_module_bindings_generated.dart abstract class ShareModuleBindings extends WebFBaseModule { ShareModuleBindings(super.moduleManager); @override String get name => 'Share'; Future<bool> share(NativeByteData imageData, dynamic text, dynamic subject); Future<bool> shareText(ShareTextOptions? options); Future<ShareSaveResult> save(NativeByteData imageData, dynamic filename); Future<ShareSaveResult> saveForPreview(NativeByteData imageData, dynamic filename); }

You implement your module by extending this base:

// lib/src/share_module.dart class ShareModule extends ShareModuleBindings { ShareModule(super.moduleManager); @override Future<bool> share(NativeByteData imageData, dynamic text, dynamic subject) { final textStr = (text ?? '').toString(); final subjectStr = (subject ?? '').toString(); return handleShare(imageData, textStr, subjectStr); } @override Future<bool> shareText(ShareTextOptions? options) { final title = options?.title ?? ''; var text = options?.text ?? ''; final url = options?.url; return handleShareText(title, text, url: url); } @override Future<ShareSaveResult> save(NativeByteData imageData, dynamic filename) { final effectiveFilename = filename?.toString(); return handleSaveScreenshot(imageData, effectiveFilename); } @override Future<ShareSaveResult> saveForPreview(NativeByteData imageData, dynamic filename) { final effectiveFilename = filename?.toString(); return handleSaveForPreview(imageData, effectiveFilename); } }

The deeplink module (DeepLinkModule) follows the same pattern, extending DeepLinkModuleBindings with:

@override Future<OpenDeepLinkResult> openDeepLink(OpenDeepLinkOptions? options) async { ... } @override Future<RegisterDeepLinkHandlerResult> registerDeepLinkHandler( RegisterDeepLinkHandlerOptions? options, ) async { ... }

The generated bindings also provide fromMap / toMap / toJson helpers so complex results are easy to pass between Dart and JS.

Step 4: Use the Generated npm Package in JS/TS

Once the module package is built and (optionally) published, you can use it from your WebF app:

npm install @openwebf/webf-share
import { WebFShare, ShareHelpers } from '@openwebf/webf-share'; // Share text const ok = await WebFShare.shareText({ title: 'My App', text: 'Check this out!', url: 'https://example.com', }); // Save screenshot const result = await WebFShare.saveScreenshot({ imageData, filename: 'my_screenshot', });

For deeplinks:

npm install @openwebf/webf-deeplink
import { WebFDeepLink, DeepLinkHelpers } from '@openwebf/webf-deeplink'; const result = await WebFDeepLink.openDeepLink({ url: 'whatsapp://send?text=Hello', fallbackUrl: 'https://wa.me/?text=Hello', }); if (result.success) { console.log('Deep link opened successfully'); } else { console.error('Failed:', result.error); }

Complete Example: Share Module

TypeScript Definition

// lib/src/share.module.d.ts interface ShareTextOptions { title?: string; text?: string; url?: string; } interface ShareSaveResult { success: string; filePath?: string; platformInfo?: string; message: string; error?: string; } interface WebFShare { share( imageData: ArrayBuffer | Uint8Array, text: string, subject?: string ): Promise<boolean>; shareText(options: ShareTextOptions): Promise<boolean>; save( imageData: ArrayBuffer | Uint8Array, filename?: string ): Promise<ShareSaveResult>; saveForPreview( imageData: ArrayBuffer | Uint8Array, filename?: string ): Promise<ShareSaveResult>; }

Generated Dart Bindings

After running webf module-codegen, you’ll have:

// lib/src/share_module_bindings_generated.dart abstract class ShareModuleBindings extends WebFBaseModule { ShareModuleBindings(super.moduleManager); @override String get name => 'Share'; Future<bool> share(NativeByteData imageData, dynamic text, dynamic subject); Future<bool> shareText(ShareTextOptions? options); Future<ShareSaveResult> save(NativeByteData imageData, dynamic filename); Future<ShareSaveResult> saveForPreview(NativeByteData imageData, dynamic filename); } class ShareTextOptions { String? title; String? text; String? url; ShareTextOptions({this.title, this.text, this.url}); factory ShareTextOptions.fromMap(Map<String, dynamic> map) { return ShareTextOptions( title: map['title'] as String?, text: map['text'] as String?, url: map['url'] as String?, ); } Map<String, dynamic> toMap() { return { 'title': title, 'text': text, 'url': url, }; } } class ShareSaveResult { String success; String? filePath; String? platformInfo; String message; String? error; ShareSaveResult({ required this.success, this.filePath, this.platformInfo, required this.message, this.error, }); factory ShareSaveResult.fromMap(Map<String, dynamic> map) { return ShareSaveResult( success: map['success'] as String, filePath: map['filePath'] as String?, platformInfo: map['platformInfo'] as String?, message: map['message'] as String, error: map['error'] as String?, ); } Map<String, dynamic> toMap() { return { 'success': success, 'filePath': filePath, 'platformInfo': platformInfo, 'message': message, 'error': error, }; } Map<String, dynamic> toJson() => toMap(); }

Implementation

// lib/src/share_module.dart import 'package:share_plus/share_plus.dart'; import 'package:webf/webf.dart'; import 'share_module_bindings_generated.dart'; class ShareModule extends ShareModuleBindings { ShareModule(super.moduleManager); @override Future<bool> share(NativeByteData imageData, dynamic text, dynamic subject) async { try { final bytes = imageData.data; final textStr = (text ?? '').toString(); final subjectStr = (subject ?? '').toString(); final xFile = XFile.fromData( bytes, name: 'shared_image.png', mimeType: 'image/png', ); await Share.shareXFiles( [xFile], text: textStr, subject: subjectStr, ); return true; } catch (e) { print('Share error: $e'); return false; } } @override Future<bool> shareText(ShareTextOptions? options) async { try { final title = options?.title ?? ''; var text = options?.text ?? ''; final url = options?.url; if (url != null && url.isNotEmpty) { text = text.isEmpty ? url : '$text\n$url'; } await Share.share(text, subject: title); return true; } catch (e) { print('Share text error: $e'); return false; } } @override Future<ShareSaveResult> save(NativeByteData imageData, dynamic filename) async { // Implementation for saving to gallery // ... return ShareSaveResult( success: 'true', filePath: '/path/to/saved/file', message: 'Image saved successfully', ); } @override Future<ShareSaveResult> saveForPreview(NativeByteData imageData, dynamic filename) async { // Implementation for saving to temp directory for preview // ... return ShareSaveResult( success: 'true', filePath: '/path/to/preview/file', message: 'Preview ready', ); } }

Registration

// lib/webf_share.dart import 'package:webf/webf.dart'; import 'src/share_module.dart'; void installWebFShare() { WebF.defineModule((moduleManager) => ShareModule(moduleManager)); } // In your main.dart void main() { installWebFShare(); WebFControllerManager.instance.initialize(...); runApp(MyApp()); }

Usage in React

import React, { useState } from 'react'; import { WebFShare } from '@openwebf/webf-share'; function ShareExample() { const [result, setResult] = useState(''); const shareText = async () => { const success = await WebFShare.shareText({ title: 'Check this out!', text: 'This is an amazing app built with WebF', url: 'https://webf.dev', }); setResult(success ? 'Shared successfully' : 'Share failed'); }; const shareImage = async () => { // Get image data from canvas or other source const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; const blob = await new Promise<Blob>((resolve) => canvas.toBlob(resolve, 'image/png') ); const arrayBuffer = await blob.arrayBuffer(); const success = await WebFShare.share( arrayBuffer, 'Check out this image!', 'Shared from my app' ); setResult(success ? 'Image shared successfully' : 'Share failed'); }; return ( <div> <button onClick={shareText}>Share Text</button> <button onClick={shareImage}>Share Image</button> <p>{result}</p> </div> ); }

TypeScript Definition

// lib/src/deeplink.module.d.ts interface OpenDeepLinkOptions { url: string; fallbackUrl?: string; } interface OpenDeepLinkResult { success: boolean; error?: string; } interface RegisterDeepLinkHandlerOptions { callback: (url: string) => void; } interface RegisterDeepLinkHandlerResult { success: boolean; } interface WebFDeepLink { openDeepLink(options: OpenDeepLinkOptions): Promise<OpenDeepLinkResult>; registerDeepLinkHandler(options: RegisterDeepLinkHandlerOptions): Promise<RegisterDeepLinkHandlerResult>; }

Implementation

// lib/src/deeplink_module.dart import 'package:url_launcher/url_launcher.dart'; import 'package:webf/webf.dart'; import 'deeplink_module_bindings_generated.dart'; class DeepLinkModule extends DeepLinkModuleBindings { DeepLinkModule(super.moduleManager); @override Future<OpenDeepLinkResult> openDeepLink(OpenDeepLinkOptions? options) async { if (options == null || options.url.isEmpty) { return OpenDeepLinkResult( success: false, error: 'URL is required', ); } try { final uri = Uri.parse(options.url); final canLaunch = await canLaunchUrl(uri); if (canLaunch) { await launchUrl(uri, mode: LaunchMode.externalApplication); return OpenDeepLinkResult(success: true); } else if (options.fallbackUrl != null && options.fallbackUrl!.isNotEmpty) { final fallbackUri = Uri.parse(options.fallbackUrl!); await launchUrl(fallbackUri, mode: LaunchMode.externalApplication); return OpenDeepLinkResult(success: true); } else { return OpenDeepLinkResult( success: false, error: 'Cannot launch URL: ${options.url}', ); } } catch (e) { return OpenDeepLinkResult( success: false, error: e.toString(), ); } } @override Future<RegisterDeepLinkHandlerResult> registerDeepLinkHandler( RegisterDeepLinkHandlerOptions? options, ) async { // Implementation would integrate with app_links or similar package // to handle incoming deep links return RegisterDeepLinkHandlerResult(success: true); } }

Usage in React

import React from 'react'; import { WebFDeepLink } from '@openwebf/webf-deeplink'; function DeepLinkExample() { const openWhatsApp = async () => { const result = await WebFDeepLink.openDeepLink({ url: 'whatsapp://send?text=Hello', fallbackUrl: 'https://wa.me/?text=Hello', }); if (!result.success) { console.error('Failed to open WhatsApp:', result.error); } }; const openMaps = async () => { const result = await WebFDeepLink.openDeepLink({ url: 'geo:37.7749,-122.4194', fallbackUrl: 'https://maps.google.com/?q=37.7749,-122.4194', }); if (!result.success) { console.error('Failed to open maps:', result.error); } }; return ( <div> <button onClick={openWhatsApp}>Open WhatsApp</button> <button onClick={openMaps}>Open Maps</button> </div> ); }

Best Practices

Module Design

  1. Keep modules focused - Each module should have a single, clear responsibility
  2. Use TypeScript definitions - Always define your API in .module.d.ts files
  3. Handle errors gracefully - Return meaningful error messages in result objects
  4. Document your APIs - Add JSDoc comments to TypeScript definitions

Type Safety

  1. Use generated bindings - Don’t write bindings manually
  2. Validate inputs - Check parameters in Dart implementation
  3. Type complex results - Use result objects instead of raw maps
  4. Leverage code generation - Let webf module-codegen handle serialization

Performance

  1. Minimize bridge calls - Batch operations when possible
  2. Use async appropriately - Don’t block the UI thread
  3. Cache results - Store expensive computation results
  4. Stream large data - Use chunking for large data transfers

Next Steps