
A watch app is not a second form in the phone process. It is another application on another device, with its own storage, startup sequence, and periods when the other side is unreachable.
PR #5487 now builds an Apple Watch companion from codename1.watchMain. On Wear OS, the same entry point becomes the Android product when codename1.watchStandalone=true; a companion Wear APK beside the phone application is not generated yet. The release also adds one phone-to-watch API that maps to WCSession on Apple platforms and the Wearable Data Layer on Android.
For encrypted SQLite and the rest of this week’s work, see the weekly release overview.
One entry point builds the watch application
The watch application starts from a fully qualified class name:
codename1.watchMain=com.example.MyWatchApp
# For a standalone Wear OS product:
codename1.watchStandalone=true
On Apple platforms, watchMain adds a companion target to the phone build. On Android, watchStandalone=true replaces the phone product with the Wear OS application rooted at watchMain. Without that flag, the Android build remains the phone application. The build logs that no companion Wear artifact was produced instead of quietly implying otherwise.
The Apple build derives the watch bundle identifier, deployment target, signing team, and display name from settings the project already has.
The phone and watch share source files, resources, CSS, and themes. They do not share runtime state. Each has its own Storage, Preferences, and SQLite files.
resources and CSS] --> B[Phone application] A --> C[Watch application
watchMain] B <-->|sendMessage
live request and reply| C B <-->|putData
latest replicated state| C B <-->|transferFile
background payload| C
Wear OS reuses the Android port. watchOS uses a separate Core Graphics renderer because it has no UIKit view hierarchy, OpenGL ES, or Metal. The watch runtime sits inside a SwiftUI shell and runs its own ParparVM translation rooted at the watch entry point.
A message and a value solve different problems
The platforms offer several transports because a watch spends much of its life asleep.
Use putData() for state that should converge when the watch next wakes:
WearableConnection.putData(new WearableMessage("/steps")
.put("count", stepCount)
.put("goalReached", stepCount >= 10000));
Register the listener during init(). A payload can be the reason the platform started the process, so listeners attached from a later form may miss the replay window.
WearableConnection.addDataListener(new WearableDataListener() {
public void dataChanged(WearableMessage data) {
if ("/steps".equals(data.getPath())) {
stepsLabel.setText("" + data.getInt("count", 0));
}
}
public void dataRemoved(String path) {
if ("/steps".equals(path)) {
stepsLabel.setText("--");
}
}
});
Each data path holds the latest value. Two rapid writes can arrive as one update. That is correct for a step count and wrong for a queue of events.
Use sendMessage() when both applications must be awake and the sender needs an answer now:
WearableConnection.sendMessage(
new WearableMessage("/workout/start"),
new WearableReplyHandler() {
public void replyReceived(WearableMessage reply) {
showWorkout(reply.getString("id", null));
}
public void replyFailed(String message) {
showReplicatedWorkoutState();
}
});
Failure is a normal branch. The phone may be asleep, out of range, or running an older version that does not know the message path. Do not use isReachable() as a preflight for a request with a fallback. Reachability can change after it is checked, and its first value during a cold start may still be unknown. Let replyFailed() select the replicated state instead. transferFile() covers files and large payloads that can arrive later.
The simulator runs two processes
The Watch > Launch Watch App command starts the watch beside the phone. The applications run in separate processes and connect through the desktop bridge, so sendMessage() and putData() take the same asynchronous route the application code expects on a device.
The simulator includes Apple Watch 41 mm and 45 mm skins, plus round and square Wear skins. Test the round skin even if the first target is Apple Watch. It catches layouts that depend on rectangular corners.
CN.isWatch() selects the form-factor-specific UI. The watch theme override changes styling without forking the rest of the theme:
Form form = new Form(BoxLayout.y());
if (CN.isWatch()) {
form.add(new Label("Hi Watch"));
form.getToolbar().setVisible(false);
} else {
form.add(new SpanLabel("Welcome to the phone application"));
}
form.show();
Complications reuse the surfaces model
A complication is a small system-rendered surface driven by a timeline. That is the same model Codename One uses for widgets, Live Activities, and Dynamic Island content.
WidgetKind steps = new WidgetKind("steps")
.setDisplayName("Steps")
.addSupportedSize(WidgetSize.WATCH_CIRCULAR)
.addSupportedSize(WidgetSize.WATCH_RECTANGULAR);
The watch sizes belong to WidgetSize instead of a second complication API. Application content can therefore share the same surface descriptors and timeline logic.
The system targets that render those watch families are not generated yet. watchOS still needs its WidgetKit extension target, and Wear OS still needs complication or tile services. The API establishes the common model without claiming those final platform adapters have shipped.
Android has one more current limit. Standalone Wear applications build today. A companion configuration does not yet produce a second Wear APK beside the phone APK. Apple Watch supports both companion and standalone targets, although standalone App Store submission still needs a manual archive step in Xcode.
Share code without pretending the watch is a phone
The watch and phone are separate products. They can still share application rules, visual assets, and surface descriptions. WearableConnection keeps the connection between them visible in ordinary Java code.
Write once, run anywhere does not require pretending every screen has the same lifecycle. A phone message can fail. A replicated value can arrive after a relaunch. A complication can render while neither application is active. The shared code handles those cases without hiding them.
The next post keeps the Codename One renderer while restoring browser-native text behavior.
Discussion
Which watch data do you treat as replicated state, and which operations really need a live reply from the phone?