Codename One can dispatch a deep link — an iOS Universal Link, an Android
App Link, a custom-scheme URL, a push-notification payload, or anything
else the platform delivers as a URL — to a specific Form by class or by
static factory method. The same route table powers in-app navigation
through Navigation.navigate("/path"), so the application chooses
declarative or imperative per call site without maintaining two parallel
maps.
The application surface is two annotations plus a tiny Navigation
class — five static methods and one value type.
Declare a route
Annotate the target Form class:
@Route("/users/:id")
public class ProfileForm extends Form {
public ProfileForm(@RouteParam("id") String id) {
setTitle("Profile " + id);
// null
}
}
Or annotate a static factory method:
public class Routes {
@Route("/home")
public static Form home() {
return new HomeForm();
}
@Route("/users/:id")
public static Form profile(@RouteParam("id") String id) {
return new ProfileForm(id);
}
}
Both forms are supported and a project can mix them. Path variables in
the pattern (:id, , *) are matched by name against parameters
annotated with @RouteParam. Missing @RouteParam on a path-variable
parameter fails the build.
Path syntax
| Pattern | Matches | Bound |
|---|---|---|
|
| — |
|
|
|
|
|
|
|
|
|
Query string parameters are bound the same way as path variables. The build prefers a path-variable match for a given name and falls back to the query string when the pattern doesn’t include that name.
Navigate from app code
The Navigation class is the imperative counterpart to @Route.
Calling Navigation.navigate("/users/42") from anywhere in the app
resolves the URL through the same generated route table that handles
incoming deep links, builds the matching Form, pushes it onto the
navigation stack, and shows it.
The five-method surface (navigate, back, getCurrent, getStack,
popTo) is everything the application sees — the rest of the URL-to-
form machinery is generated. Applications that prefer raw
new MyForm().show() keep working unchanged; only URL-driven calls
update the Navigation stack.
How it works
Codename One scans the project’s compiled bytecode at build time and
generates an internal dispatcher class from every @Route declaration.
The dispatcher is bound into the per-platform application stub before
Display.init(…), so deep links delivered during launch see the route
table immediately. The validation gate catches every problem in a single
build run:
@Routedeclared on a class that doesn’t extendFormPattern with no leading
/or empty valuePath variable with no matching
@RouteParamon the constructor / factoryDuplicate pattern declared on two different targets
@Routeon an abstract class or non-public staticmethod
iOS Universal Links
Host an apple-app-site-association JSON file at
https://your.domain/.well-known/apple-app-site-association over HTTPS
without redirects. The plugin’s AasaBuilder produces the payload:
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.app"],
"components": [
{ "/": "/users/*" },
{ "/": "/promo/*" }
]
}
]
}
}
Tell iOS which domains your app claims by setting the
ios.associatedDomains build hint — a comma-separated list of
applinks: entries. The iOS builder writes the Associated Domains
entitlement so the resulting .ipa is signed for those domains
automatically.
codename1.arg.ios.associatedDomains=applinks:example.com,applinks:www.example.com
Android App Links
Host an assetlinks.json file at
https://your.domain/.well-known/assetlinks.json. The plugin’s
AssetLinksBuilder produces the payload:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.app",
"sha256_cert_fingerprints": ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5", "7B:5A:1F:C9:0E:52:44:7C:82:19:6E:38:0A:B7:D4:11:5C:63:9F:28:E1:47:8B:30:A6:C2:5D:94:F0:6B:38:19"]
}
}
]
Tell Android which URLs to intercept by setting the
android.xintent_filter build hint. The Android builder injects the
value into the manifest verbatim, so it’s the whole <intent-filter>
element, with \n for the line breaks:
codename1.arg.android.xintent_filter=<intent-filter android:autoVerify="true">\n <action android:name="android.intent.action.VIEW"/>\n <category android:name="android.intent.category.DEFAULT"/>\n <category android:name="android.intent.category.BROWSABLE"/>\n <data android:scheme="https" android:host="your.domain" android:pathPrefix="/users"/>\n</intent-filter>
Three parts of that carry weight. android:autoVerify="true" is what makes
Android fetch the assetlinks.json above at install time and route the link
straight into your app. Leaving it out doesn’t fall back to an app chooser:
from Android 12 onward an unverified web link goes to the default browser, and
your filter never sees it until the user turns the association on by hand under
Settings > Apps > Open by default. The <data> element needs
android:scheme="https" and the host, and narrowing it with
android:pathPrefix keeps the app from claiming every URL on the domain. And
the hint holds one value, so an app that also registers a custom scheme puts
both <intent-filter> elements in the same string rather than setting the hint
twice.
The SHA-256 fingerprint comes from keytool -list -v -keystore …, or
from the Play Console under Setup > App integrity when using Play
App Signing.
Testing
iOS Simulator
xcrun simctl openurl booted "https://example.com/users/42"
Android Emulator
adb shell am start -a android.intent.action.VIEW \
-d "https://example.com/users/42" com.example.app
Desktop Simulator
Pass --cn1-arg=https://example.com/users/42 on the run command, or use
the simulator’s URL-injection helper.
Back-press guard
Independent of deep linking, individual forms can intercept back / pop attempts so they can confirm before discarding unsaved work:
editForm.setPopGuard(new PopGuard() {
public boolean canPop(Form form, PopReason reason) {
if (!isDirty()) {
return true;
}
// Discard is the ok command, so it returns true and the pop
// proceeds. The back key fires the cancel command, which means
// dismissing this dialog keeps the user on the form.
return Dialog.show("Discard changes?", "You have unsaved edits.",
"Discard", "Stay");
}
});
The guard fires for the toolbar back button, the Android hardware back
key, the iOS edge-swipe gesture, and any programmatic back navigation.
PopReason distinguishes the trigger so a guard can be selective.