Dart and Flutter bindings for nativeapi — unified access to native system APIs: windows, tray icons, menus, displays, keyboard, dialogs, storage and more.
| Android | iOS | Linux | macOS | Windows |
|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | ✅ |
🚧 Work in Progress: this package is under active development.
English | 简体中文
| Package | What it is |
|---|---|
nativeapi |
The API: windows, tray icons, menus, displays, keyboard, dialogs, storage and more. Plain Dart, usable without Flutter. |
cnativeapi |
Raw FFI bindings to core's C ABI; its build hook compiles core. Used by nativeapi. |
nativeapi_flutter |
For Flutter apps: re-exports nativeapi and adds widgets, dart:ui conversions and the multi-window bridge. |
A Flutter app:
flutter pub add nativeapi_flutterA Dart app (command line, or any other Dart host):
dart pub add nativeapinativeapi has its own Point, Size, Rectangle and Color types. nativeapi_flutter converts them to and from dart:ui (window.bounds.toRect(), Offset(10, 20).toNative()), and leaves out the nativeapi names that Flutter already uses (Brightness, Color, Display, Image, ModifierKey, ShortcutManager, Size); import package:nativeapi/nativeapi.dart with a prefix to name one of those.
import 'package:nativeapi/nativeapi.dart';
for (final display in DisplayManager.instance.getAll()) {
print('${display.name ?? ''}: ${display.size.width}x${display.size.height}');
}With package:nativeapi_flutter/nativeapi_flutter.dart, wrap a custom title bar in DragToMoveArea to move the window by dragging (double tap to maximize/restore), and the window content in DragToResizeArea to resize from its edges and corners:
DragToResizeArea(
resizeEdgeSize: 8,
child: Column(
children: [
DragToMoveArea(
child: SizedBox(height: 40, child: Center(child: Text('My window'))),
),
Expanded(child: MyContent()),
],
),
)Both widgets use WindowManager.instance.getCurrent() unless a window is passed. Moving via DragToMoveArea is not yet implemented on Linux.
Window.create() opens a bare native window with no Flutter view in it. To render widgets in a second window, create it with Flutter's multi-window API and hand the controller to nativeapi:
import 'package:flutter/src/foundation/_features.dart' show isWindowingEnabled;
import 'package:flutter/src/widgets/_window.dart' as fw;
import 'package:nativeapi_flutter/nativeapi_flutter.dart';
import 'package:nativeapi_flutter/windowing.dart';
// Before WidgetsFlutterBinding.ensureInitialized(): stable has no
// `flutter config --enable-windowing`, so the app switches the API on itself.
isWindowingEnabled = true;
final controller = fw.RegularWindowController(
size: const Size(320, 48),
title: 'Toolbar',
);
// In the widget tree: fw.RegularWindow(controller: controller, child: ...)
final window = controller.nativeWindow; // a nativeapi Window, same id as WindowManager's
window?.titleBarStyle = TitleBarStyle.hidden;
window?.isAlwaysOnTop = true;All windows share one engine and one isolate, so they talk to each other through ordinary Dart objects — no runner changes, no message channels. Flutter's multi-window API is experimental and internal to the framework, which is why the bridge lives in its own library, package:nativeapi_flutter/windowing.dart. It is written against the stable channel (checked with 3.47.5); stable does not offer flutter config --enable-windowing, so the examples set Flutter's internal isWindowingEnabled in main(). See floating_toolbar_example for a child window built this way, and browser_tabs_example and detachable_window_example.
A Dart program without Flutter can open windows too. Hand the app to runNativeApp(), which runs it on the platform's UI thread and keeps the platform loop turning between Dart's timers and futures:
import 'package:nativeapi/nativeapi.dart';
void app() {
final window = Window.create()!..title = 'Hello';
final root = window.contentView!
..layout = ViewLayout.column
..padding = const EdgeInsets(top: 16, right: 16, bottom: 16, left: 16)
..spacing = 8;
final label = Label.create('Not clicked yet')!;
var clicks = 0;
final button = Button.create('Click me')!
..addListener((event) {
if (event is ButtonClickedEvent) label.text = 'Clicked ${++clicks}×';
});
root
..addSubview(label)
..addSubview(button);
window.show();
}
void main() => runNativeApp(app);app must be a top-level or static function: it runs in an isolate of its own. On macOS that isolate lives on the process's first thread, the only one AppKit accepts, which the Dart VM otherwise keeps parked. The process ends when the app quits (Application.instance.quit(), Cmd+Q). See dart_view_example for a larger app built this way.
The examples are the flutter_* directories in the repository's examples/, plus dart_view_example, a plain Dart program (dart run bin/main.dart). Each Flutter one is an app for one module; they resolve through the pub workspace at the repository root:
flutter pub get # at the repository root
cd examples/flutter_display_example
flutter runDevelopment happens in nativeapi, which holds every binding and the code generator and checks out the core library as a submodule:
git clone --recursive https://github.com/libnativeapi/nativeapi.gitFiles marked AUTO-GENERATED. DO NOT EDIT. are generated from the C++ headers in nativeapi. To change the API, send a pull request there; maintainers regenerate the bindings.
- API requests and native behavior bugs → nativeapi-core issues
- Bugs specific to one binding → nativeapi issues
- Not sure → nativeapi-core issues