Codename One builds a Flutter application from its Dart source. The Dart is translated to Java at build time, and the result is an ordinary Codename One application: no Dart VM, no embedded rendering engine and no platform view ship with it. It builds for every platform Codename One targets, and it comes out smaller, quicker to start and lighter on memory than the same application built by Flutter.

Dart support is experimental. Most widgets render, but some callbacks and parameters are accepted and then ignored, and the pipeline has so far been verified against one large sample application. Test every interaction your screens depend on before shipping.

Smaller, faster and lighter than Flutter

The charts below come from one application, the Flutter Gallery, built both ways from the same Dart source: once by the Flutter toolchain’s own release build, and once by Codename One. Both builds are measured on the same machine, in interleaved rounds, on every change to the project. Lower is better throughout.

Installed size of the Flutter Gallery per platform: Codename One against Flutter
Executable code of the Flutter Gallery per platform: Codename One against Flutter
Cold start to the first frame per platform: Codename One against Flutter
Memory at rest per platform: Codename One against Flutter

The comparison is tilted toward Flutter. The application is Flutter’s own showcase, written by the Flutter team in Flutter’s idioms, and Codename One runs it as it is. To do that the Codename One build carries a runtime that reimplements Flutter’s widget, element and layout model on top of Codename One components, which is work an application written for Codename One never pays for. The same screens written directly against Codename One’s own components would be smaller and faster again. The numbers above are what a team gets on the day it moves its Flutter code over, before changing a line of it.

Two platforms aren’t measured in full, and the charts leave them out rather than estimate them:

  • Start-up and memory on iOS. Flutter can’t compile Dart ahead of time for the simulator, so a simulator run would time Flutter’s debug engine against a Codename One release build. iOS sizes come from release device bundles.

  • Start-up and memory on the web. Timing a browser build needs a driver for the browser’s developer tools, which the benchmark doesn’t have, so it reports web sizes only.

Android start-up and memory come from an emulator rather than a phone. They compare the two builds on one machine; they aren’t phone timings. How the numbers are measured explains how each metric is taken.

What it looks like

The Flutter Gallery’s demos, rendered by Flutter on the left and by Codename One on the right from the same Dart source:

The Flutter Gallery home screen rendered by Flutter and by Codename One
The Cards demo rendered by Flutter and by Codename One
The Text fields demo rendered by Flutter and by Codename One
The Bottom navigation demo rendered by Flutter and by Codename One
The Colors demo rendered by Flutter and by Codename One
The Text button demo rendered by Flutter and by Codename One
The Cupertino buttons demo rendered by Flutter and by Codename One
The Shrine study’s login screen rendered by Flutter and by Codename One
The Reply study’s inbox rendered by Flutter and by Codename One

The Codename One side is a real Codename One component tree, not a picture of one: it uses the theme, the event dispatch thread, the accessibility tree and the native build like any other Codename One screen, and it can share a Form with hand-written Codename One code.

Importing a Flutter application

One command brings a Flutter project into a Codename One project:

mvn cn1:import-flutter-project -Dcn1.flutter.import=/path/to/flutter_app

Run it in a project generated from the Codename One archetype or the initializr. It does what wiring Flutter in by hand would take:

  • Copies the Dart source in lib into common/src/main/flutter.

  • Copies every asset and font pubspec.yaml declares, including whole asset directories and the assets of the packages the application depends on, which it finds through the project’s .dart_tool/package_config.json or the pub cache.

  • Adds the Flutter runtime to common/pom.xml and makes sure the build translates the Dart.

  • Writes the application’s entry class so that it runs the Dart main(). A class that was already there is saved beside it with a .pre-flutter-import suffix.

  • Carries the application name and version over from pubspec.yaml.

It then lists the application’s pub dependencies and says which of them Codename One provides. The transpiler doesn’t compile a package’s Dart; a package works when the Flutter runtime implements its API, as it does for provider, intl, animations and other common ones. A dependency listed as not covered is the first thing to look at if a screen misbehaves.

Re-running the command later picks up changes made on the Flutter side. A file you edited after the earlier import is left as you edited it, and a file the Flutter project no longer has is removed.

No Dart or Flutter SDK is needed to build the result. The transpiler is Java and runs as part of the normal Maven build. The command reads the pub cache only to find package assets, so run flutter pub get once in the Flutter project if it reports a package it can’t find.

Migrating an entire application

After the import, the Dart is the application: the entry class calls the Dart main(), whose runApp mounts the widget tree as the root of a Form. Build and run it like any Codename One application, in the simulator or for a device.

FlutterUI.runApp is the same call from Java, the analog of Flutter’s own runApp. It installs the Material base theme, because the widget tree is expected to be the whole UI and to look the way its author intended:

// runApp mounts the tree in a Form of its own and shows it. Unlike
// wrap(), this installs the Material base theme, because the widget
// tree is expected to be the whole UI.
FlutterUI.runApp(new Text("The entire application"));

A port is mostly a question of how much of the Dart language and of the Flutter libraries the application uses. The transpiler reports what it can’t handle rather than guessing: a construct outside the supported subset fails the build with a code naming the feature, so the gaps are a list to work through rather than a runtime surprise. At runtime, a widget that isn’t implemented is recorded through the error inventory instead of drawing nothing.

Migrating a single form

The same build can carry Dart screens inside an application otherwise written in Codename One, so a migration can go one screen at a time in either direction.

Put the Dart files under common/src/main/flutter; the import command does this, or copy them by hand. Each Dart widget class becomes a Java class of the same name in com.codename1.generated.flutter, with its parameters as constructor arguments in declaration order and the widget key first. A Dart MyHomePage({super.key, required this.title}) is created from Java as new MyHomePage(null, "Inbox").

FlutterUI.wrap inflates the widget into a plain Container, which goes anywhere an ordinary component can:

Form dashboard = new Form("Dashboard", BoxLayout.y());
dashboard.add(new Label("Written in Codename One"));

// A Dart widget tree, compiled to Java at build time. wrap() returns
// an ordinary Container, so it is added like any other component.
dashboard.add(FlutterUI.wrap(new Text("Rendered from Dart")));

dashboard.add(new Button("Also Codename One"));
dashboard.show();

The wrapped widget is a component like any other: it takes part in the parent’s layout, scrolls with it and is styled by the same theme. It doesn’t install the Material base theme, so a Dart screen dropped into an existing form won’t restyle the rest of it.

A wrapped widget isn’t inside a navigator, so it has no Overlay of its own. Dart code that calls Overlay.of(context) from inside it gets the same error it would get outside a Navigator on Flutter; show such a screen as a route of its own instead.

Calling between Java and Dart

Java and Dart run in the same application, on the same thread, so calls between them are ordinary method calls. There are three ways to make them, and the first is the one a Flutter developer already knows.

Platform channels

A Flutter application talks to its platform code through MethodChannel and EventChannel from package:flutter/services.dart. Those work unchanged: the Dart side compiles unchanged, and the platform side, which is Kotlin or Swift in a Flutter project, becomes Java in the Codename One project. The Java classes in com.codename1.flutter.platform mirror Flutter’s Android io.flutter.plugin.common API, so Android handler code ports by changing its imports.

Dart calling Java. The Dart side invokes a method on a named channel:

// Calls the Java handler registered with MethodChannel.setMethodCallHandler
// on the platform side (FlutterInteropSnippets.answerDartWithAJavaHandler).
final batteryChannel = MethodChannel('com.example.app/battery');

Future<int> getBatteryLevel() async {
  final level = await batteryChannel.invokeMethod('getBatteryLevel');
  return level as int;
}

A Java handler registered under the same channel name answers it:

MethodChannel channel = new MethodChannel("com.example.app/battery");
channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
    @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        if ("getBatteryLevel".equals(call.method())) {
            result.success(87);
        } else {
            result.notImplemented();
        }
    }
});

result.error(…​) reaches Dart as a PlatformException, and result.notImplemented(), or no handler at all, as a MissingPluginException, as on Flutter. The handler doesn’t have to answer before it returns: keep the Result and complete it when the answer arrives.

Java calling Dart. The direction reverses with the same two classes. Dart registers a handler on the channel:

// Answers calls the Java side makes with MethodChannel.invokeMethod
// (FlutterInteropSnippets.invokeADartRegisteredHandler).
void installBatteryRefreshHandler() {
  batteryChannel.setMethodCallHandler((MethodCall call) async {
    if (call.method == 'refreshBatteryLevel') {
      return getBatteryLevel();
    }
    throw MissingPluginException();
  });
}

Java invokes it and gets the reply through a callback:

MethodChannel channel = new MethodChannel("com.example.app/battery");
channel.invokeMethod("refreshBatteryLevel", null, new MethodChannel.Result() {
    @Override
    public void success(Object result) {
        System.out.println("Dart replied: " + result);
    }

    @Override
    public void error(String errorCode, String errorMessage, Object errorDetails) {
        System.out.println("Dart reported an error: " + errorCode);
    }

    @Override
    public void notImplemented() {
        System.out.println("no Dart handler is registered");
    }
});

Events from Java to Dart. An EventChannel carries a stream of events: a sensor reading, a download’s progress, a message from a server. Java pushes them through the EventSink it receives when Dart starts listening:

EventChannel channel = new EventChannel("com.example.app/sensor");
final EventChannel.EventSink[] activeSink = new EventChannel.EventSink[1];
channel.setStreamHandler(new EventChannel.StreamHandler() {
    @Override
    public void onListen(Object arguments, EventChannel.EventSink events) {
        activeSink[0] = events;
    }

    @Override
    public void onCancel(Object arguments) {
        activeSink[0] = null;
    }
});

// Called whenever a new reading arrives, from wherever that is.
EventChannel.EventSink sink = activeSink[0];
if (sink != null) {
    sink.success(42.5);
}

Dart listens to the stream as it would on Flutter:

// Listens to events the Java side pushes through an EventSink
// (FlutterInteropSnippets.pushEventsFromAJavaStreamHandler).
final sensorChannel = EventChannel('com.example.app/sensor');

StreamSubscription<dynamic> listenToSensorReadings(void Function(double value) onReading) {
  return sensorChannel.receiveBroadcastStream().listen((event) {
    onReading(event as double);
  });
}

onListen runs when the first listener subscribes and onCancel when the last one cancels, so a sensor or a connection is only active while something is listening.

Values cross the channel in the types each side expects, as Flutter’s message codec does:

DartJava

int

Long. An Integer, Short or Byte sent from Java arrives as int.

double

Double. A Float sent from Java arrives as double.

String, bool, null

String, Boolean, null

List, Map

java.util.List, java.util.Map, converted element by element

Every handler runs on the event dispatch thread, and a reply is always delivered asynchronously, never inside the call that asked for it, as on Flutter.

Calling Dart functions directly

Without a channel, Java can call transpiled Dart directly. Each Dart file becomes a class named after it, with Lib appended: lib/greeting.dart becomes LibGreetingLib, and its top-level functions are public static methods of that class. Dart’s main is renamed main$.

String greeting = LibGreetingLib.greet("World");
System.out.println(greeting);

This is the quickest way to reach Dart logic that has no UI, a parser or a pricing rule, from Java. Channels are the better boundary for code that a Flutter build must also keep running, because the Dart side doesn’t change.

Sharing state and callbacks

A ValueNotifier or ChangeNotifier is an ordinary Java object on the Codename One side. Java code that holds one can set its value or call notifyListeners(), and every Dart widget listening to it, through ValueListenableBuilder or AnimatedBuilder, rebuilds. Callback parameters work the other way: an onPressed or onChanged that a Java caller passes into a Dart widget’s constructor is a Java lambda, and the Dart code calls it.

How it fits together

The pipeline has three stages, and each is an ordinary build step you can run and inspect on its own.

Dart source through the transpiler to the Codename One ports

Dart widget source in src/main/flutter is read by the transcode-flutter goal during generate-sources. It emits Java into target/generated-sources/flutter, which the normal compile phase picks up. At runtime those generated classes call into the Flutter runtime library, which implements the widget, element and render-object model on top of Codename One containers.

The key design point is where the boundary sits. The runtime reimplements Flutter’s layout and composition model — constraints down, sizes up, elements reconciled against widgets — but it doesn’t reimplement Flutter’s rasterizer. Painting is Codename One’s, which is what makes the output a real Codename One component tree rather than a texture.

The goal is bound in every project generated from the archetype and does nothing while src/main/flutter doesn’t exist. To wire a project by hand instead of importing, add the runtime dependency to the common module, where generated projects already carry it commented out:

<dependency>
    <groupId>com.codenameone</groupId>
    <artifactId>codenameone-flutter-runtime</artifactId>
    <version>${cn1.version}</version>
</dependency>

The generated package and the paths are configurable:

PropertyMeaning

cn1.flutter.sourceDir

Where the Dart lives. Defaults to src/main/flutter.

cn1.flutter.outputDir

Where the generated Java is written. Defaults to target/generated-sources/flutter.

cn1.flutter.package

The package the generated classes are emitted into. Defaults to com.codename1.generated.flutter.

How the numbers are measured

The benchmark lives in scripts/flutter-bench. It builds the Flutter Gallery both ways on each platform and measures both builds on the same machine, and continuous integration runs it on every change, publishing the numbers to the pull request and to the port status page.

What it reports, per platform:

MetricWhat it means

Installed size

The sum of the artifact’s file lengths, which is what the user’s device gives up. Not du, which rounds every file up to a block.

Executable code

Every compiled binary in the artifact, added together. Counting only the main executable is wrong on iOS, where a Flutter application’s own code isn’t in the executable at all — it’s in the frameworks beside it.

Download size

The artifact compressed, because that’s what a store ships.

Cold start

Wall time from launching the process to the first frame being on the screen, measured from outside so neither runtime is trusted to time itself.

Memory at rest

The platform’s own accounting for a settled, idle application.

Three rules keep the numbers honest, and each exists because the obvious alternative produced a flattering result:

  • Interleaved rounds. One launch of each build, alternating, repeated. A machine that gets busier halfway through then penalizes both builds equally instead of whichever happened to run second, and start-up and memory are judged round by round: the ratio is the median of the per-round ratios.

  • Start-up is a bracket where the two runtimes report different events. On the web, Flutter’s earliest marker fires before its first frame is rasterized while Codename One’s fires once the frame is on the screen, so the benchmark takes Flutter’s figure as a range and computes the ratio from the end least favorable to Codename One.

  • A platform that can’t be measured says so. iOS start-up and memory are reported as not measured rather than taken from a simulator, for the reason given above.

To run it locally against builds you already have:

python3 scripts/flutter-bench/run_bench.py --list

python3 scripts/flutter-bench/run_bench.py --platform macos \
    --cn1-app     /path/to/Bench.app \
    --flutter-app /path/to/gallery.app \
    --json out/macos.json --markdown out/macos.md

--list also reports which platform adapters have been exercised end to end, which isn’t the same question as which ones exist. The charts in Smaller, faster and lighter than Flutter are rendered from the benchmark’s result files by scripts/flutter-bench/render_doc_charts.py.