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/ServiceAutomatic 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.tsfile 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
- Generates a typed npm wrapper (e.g.
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-codegenBehavior:
- 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 callswebf.invokeModuleAsync('Share', ...)src/types.ts– exported TS interfaces such asShareTextOptions,ShareSaveResult
- Dart bindings:
lib/src/share_module_bindings_generated.dartincluding:ShareModuleBindingsShareTextOptions,ShareSaveResult, etc.
- A temporary npm project with:
- Optionally, it can publish the npm package:
--publish-to-npmpublishes 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-shareStep 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-shareimport { 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-deeplinkimport { 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>
);
}Complete Example: DeepLink Module
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
- Keep modules focused - Each module should have a single, clear responsibility
- Use TypeScript definitions - Always define your API in
.module.d.tsfiles - Handle errors gracefully - Return meaningful error messages in result objects
- Document your APIs - Add JSDoc comments to TypeScript definitions
Type Safety
- Use generated bindings - Don’t write bindings manually
- Validate inputs - Check parameters in Dart implementation
- Type complex results - Use result objects instead of raw maps
- Leverage code generation - Let
webf module-codegenhandle serialization
Performance
- Minimize bridge calls - Batch operations when possible
- Use async appropriately - Don’t block the UI thread
- Cache results - Store expensive computation results
- Stream large data - Use chunking for large data transfers
Next Steps
- Hybrid UI - Create custom Flutter widgets for WebF
- Advanced Topics - Performance monitoring and more